700字范文,内容丰富有趣,生活中的好帮手!
700字范文 > Spring Security系列(22)- Security实现手机短信登录功能

Spring Security系列(22)- Security实现手机短信登录功能

时间:2020-12-26 19:53:25

相关推荐

Spring Security系列(22)- Security实现手机短信登录功能

准备

需求

采用手机号+短信验证码登录方式是很常见的一种需求。

那我们如何在Spring Security实现这种功能呢?

表单登录流程

首先再回顾一下用户名密码表单登录流程。

登录请求进入过滤器调用认证管理器查询数据库中的信息,返回UserDetailsAuthenticationProvider校验请求和数据库中的密码是否配置封装已认证信息

实现思路

我们可以完全按照表单登录,自定义过滤器、校验器等,实现短信登录功能。

案例演示

1. 获取验证码

登录页中,用户填写手机号后,会有一个获取验证码的按钮。

我们编写一个获取验证码的接口,模拟短信平台发送验证码,然后将验证码放在缓存中。

@RestController@RequestMapping("/sms")@Slf4jpublic class SmsEndpoint {@Autowiredprivate RedisTemplate<String, String> redisTemplate;/*** 发送验证码接口** @param phone* @return*/@GetMapping("/send/code")public Map<String,String> msmCode(String phone) {// 1. 获取到手机号log.info(phone + "请求获取验证码");// 2. 模拟调用短信平台获取验证码,以手机号为KEY,验证码为值,存入Redis,过期时间一分钟String code = RandomUtil.randomNumbers(6);redisTemplate.opsForValue().setIfAbsent(phone, code, 60, TimeUnit.SECONDS);String saveCode = redisTemplate.opsForValue().get(phone);// 缓存中的codeLong expire = redisTemplate.opsForValue().getOperations().getExpire(phone, TimeUnit.SECONDS); // 查询过期时间// 3. 验证码应该通过短信发给用户,这里直接返回吧Map<String,String> result=new HashMap<>();result.put("code",saveCode);result.put("过期时间",expire+"秒");return result;}}

在WebSecurityConfigurerAdapter中放行验证码接口。

http.authorizeRequests().antMatchers("/login", "/sms/send/code","/sms/login").permitAll()

接口测试通过:

2. 编写AuthenticationToken类

参考UsernamePasswordAuthenticationToken编写一个AuthenticationToken类,这个类主要用于认证时,封装认证信息

public class SmsAuthenticationToken extends AbstractAuthenticationToken {private static final long serialVersionUID = 1L;private final Object principal;private Object credentials;public SmsAuthenticationToken(Object principal, Object credentials) {super((Collection) null);this.principal = principal;this.credentials = credentials;this.setAuthenticated(false);}public SmsAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {super(authorities);this.principal = principal;this.credentials = credentials;super.setAuthenticated(true);}@Overridepublic Object getCredentials() {return this.credentials;}@Overridepublic Object getPrincipal() {return this.principal;}@Overridepublic void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {Assert.isTrue(!isAuthenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");super.setAuthenticated(false);}@Overridepublic void eraseCredentials() {super.eraseCredentials();this.credentials = null;}}

3. 编写过滤器

参考UsernamePasswordAuthenticationFilter写一个过滤器,拦截短信登录接口/sms/login,调用认证管理器进行认证。

public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {// 设置拦截/sms/login短信登录接口private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/sms/login", "POST");// 认证参数private String phoneParameter = "phone";private String smsCodeParameter = "smsCode";private boolean postOnly = true;public SmsAuthenticationFilter() {super(DEFAULT_ANT_PATH_REQUEST_MATCHER);}public SmsAuthenticationFilter(AuthenticationManager authenticationManager) {super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);}@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {if (this.postOnly && !"POST".equals(request.getMethod())) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());} else {String phone = this.obtainPhone(request);phone = phone != null ? phone : "";phone = phone.trim();String smsCode = this.obtainSmsCode(request);smsCode = smsCode != null ? smsCode : "";SmsAuthenticationToken authRequest = new SmsAuthenticationToken(phone, smsCode);this.setDetails(request, authRequest);// 认证return this.getAuthenticationManager().authenticate(authRequest);}}@Nullableprotected String obtainSmsCode(HttpServletRequest request) {return request.getParameter(this.smsCodeParameter);}@Nullableprotected String obtainPhone(HttpServletRequest request) {return request.getParameter(this.phoneParameter);}protected void setDetails(HttpServletRequest request, SmsAuthenticationToken authRequest) {authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));}public void setPhoneParameter(String phoneParameter) {Assert.hasText(phoneParameter, "Phone parameter must not be empty or null");this.phoneParameter = phoneParameter;}public void setSmsCodeParameter(String smsCodeParameter) {Assert.hasText(smsCodeParameter, "SmsCode parameter must not be empty or null");this.smsCodeParameter = smsCodeParameter;}public void setPostOnly(boolean postOnly) {this.postOnly = postOnly;}public final String getUsernameParameter() {return this.phoneParameter;}public final String getPasswordParameter() {return this.smsCodeParameter;}}

4. 编写UserDetailsService

UserDetailsService主要负责根据用户名查询数据库信息,这里我们需要通过手机号查询用户信息。

@Service("smsUserDetailsService")public class SmsUserDetailsService implements UserDetailsService {@AutowiredUserService userService;/*** @param phone 手机号*/@Overridepublic UserDetails loadUserByUsername(String phone) throws UsernameNotFoundException {// 1. 数据库查询手机用户,这里需要写根据手机号查询用户信息,这里写死吧。。。UserInfoDO userInfoDO = userService.getUserInfoByName("test");if (userInfoDO == null) {throw new UsernameNotFoundException("手机号不存在!");}// 2. 设置权限集合,后续需要数据库查询/* List<GrantedAuthority> authorityList =maSeparatedStringToAuthorityList("role");*/// 2. 角色权限集合转为 List<GrantedAuthority>List<Role> roleList = userInfoDO.getRoleList();List<Permission> permissionList = userInfoDO.getPermissionList();List<GrantedAuthority> authorityList = new ArrayList<>();roleList.forEach(e -> {String roleCode = e.getRoleCode();if (StringUtils.isNotBlank(roleCode)) {SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(roleCode);authorityList.add(simpleGrantedAuthority);}});permissionList.forEach(e -> {String code = e.getCode();if (StringUtils.isNotBlank(code)) {SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(code);authorityList.add(simpleGrantedAuthority);}});// 3. 返回自定义的用户信息MyUser myUser = new MyUser(userInfoDO.getUserName(), userInfoDO.getPassword(), authorityList);// 设置自定义扩展信息myUser.setDeptId(userInfoDO.getOrganizationId());myUser.setLoginTime(new Date());return myUser;}}

5. 编写AuthenticationProvider

认证管理器会调用认证程序AuthenticationProvider进行认证操作,我们需要实现根据用户输入参数,校验验证码,匹配后,设置认证成功。

@Componentpublic class SmsAuthenticationProvider implements AuthenticationProvider {private UserDetailsService userDetailsServiceImpl;private RedisTemplate<String, String> redisTemplate;public SmsAuthenticationProvider(@Qualifier("smsUserDetailsService") UserDetailsService userDetailsServiceImpl, RedisTemplate<String, String> redisTemplate) {this.userDetailsServiceImpl = userDetailsServiceImpl;this.redisTemplate = redisTemplate;}@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication;Object principal = authentication.getPrincipal();// 获取凭证也就是用户的手机号String phone = "";if (principal instanceof String) {phone = (String) principal;}String inputCode = (String) authentication.getCredentials(); // 获取输入的验证码// 1. 检验Redis手机号的验证码String redisCode = redisTemplate.opsForValue().get(phone);if (StrUtil.isEmpty(redisCode)) {throw new BadCredentialsException("验证码已经过期或尚未发送,请重新发送验证码");}if (!inputCode.equals(redisCode)) {throw new BadCredentialsException("输入的验证码不正确,请重新输入");}// 2. 根据手机号查询用户信息UserDetails userDetails = userDetailsServiceImpl.loadUserByUsername(phone);if (userDetails == null) {throw new InternalAuthenticationServiceException("phone用户不存在,请注册");}// 3. 重新创建已认证对象,SmsAuthenticationToken authenticationResult = new SmsAuthenticationToken(principal,inputCode, userDetails.getAuthorities());authenticationResult.setDetails(authenticationToken.getDetails());return authenticationResult;}@Overridepublic boolean supports(Class<?> aClass) {return SmsAuthenticationToken.class.isAssignableFrom(aClass);}}

6. 编写短信认证配置类

编写短信认证配置类,将过滤器,认证程序等加入到配置中。

@Configurationpublic class SmsSecurityConfigurerAdapter extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {@AutowiredCustomAuthenticationSuccessHandler authenticationSuccessHandler;@Autowiredprivate RedisTemplate<String,String> redisTemplate;@AutowiredCustomAuthenticationFailureHandler authenticationFailureHandler;@Resourceprivate SmsUserDetailsService smsUserDetailsService;@Overridepublic void configure(HttpSecurity http) throws Exception {SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter();smsAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));smsAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);smsAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider(smsUserDetailsService,redisTemplate);http.authenticationProvider(smsAuthenticationProvider).addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);}}

将以上配置添加到主配置中,并需要在AuthenticationManager中添加DaoAuthenticationProvider,不然用户名密码登录会报错。

@Configuration@EnableWebSecurity(debug = true)public class MyWebSecurityConfiguration extends WebSecurityConfigurerAdapter {@AutowiredMyUserDetailsService myUserDetailsService;@AutowiredCustomAuthenticationFailureHandler authenticationFailureHandler;@AutowiredCustomAccessDeniedHandler accessDeniedHandler;@AutowiredCustomAuthenticationSuccessHandler authenticationSuccessHandler;@Autowiredprivate SmsSecurityConfigurerAdapter smsSecurityConfigurerAdapter;@AutowiredSmsAuthenticationProvider smsAuthenticationProvider;@Overridepublic void configure(WebSecurity web) throws Exception {super.configure(web);}@Overrideprotected void configure(HttpSecurity http) throws Exception {// 关闭csrf,开启跨域支持http.csrf().disable().cors();http.authorizeRequests().antMatchers("/login", "/sms/send/code", "/sms/login").permitAll().anyRequest().authenticated().and().formLogin().failureHandler(authenticationFailureHandler).successHandler(authenticationSuccessHandler); // 配置登录失败处理器http.exceptionHandling().accessDeniedHandler(accessDeniedHandler); // 配置DeniedHandler处理器http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);// 添加手机号短信登录http.apply(smsSecurityConfigurerAdapter);}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {// 设置自定义用户认证//auth.userDetailsService(myUserDetailsService);super.configure(auth);}/*** 将Provider添加到认证管理器中** @return* @throws Exception*/@Overrideprotected AuthenticationManager authenticationManager() throws Exception {ProviderManager authenticationManager = new ProviderManager(Arrays.asList(smsAuthenticationProvider, daoAuthenticationProvider()));authenticationManager.setEraseCredentialsAfterAuthentication(false);return authenticationManager;}/*** 注入密码解析器到IOC中*/@BeanPasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@BeanDaoAuthenticationProvider daoAuthenticationProvider() {DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());daoAuthenticationProvider.setUserDetailsService(myUserDetailsService);daoAuthenticationProvider.setHideUserNotFoundExceptions(false); // 设置显示找不到用户异常return daoAuthenticationProvider;}}

7. 测试

首先获取短信验证码:

然后调用短信登录接口,登录成功并返回了认证信息。

然后访问另一个资源接口,发现成访问到了。

使用用户名密码也可以登录:

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。