springsecurity自定义短信验证码认证登录流程
文章目录
- 前言
- 验证码存储我们采用redis作为缓存
- (注意,这里为了测试方便改为手动设置验证码)
-
- 经过上面接口,验证码已经存入到redis中,下面开始认证流程
- 自定义SmsCodeAuthenticationToken短信验证码认证token
- 有了sms认证token后,我们还要自定义登录过滤器SmsCodeAuthenticationFilter,专门用来处理短信登录路径
-
- 依然是参照security默认登录的过滤器UsernamePasswordAuthenticationFilter来实现自己的代码
- 有了这两个后,我们还需要一个处理认证的provider,自定义一个SmsCodeAuthenticationProvider
-
- 依然是参照官方默认的DaoAuthenticationProvider,来实现我们自定义的
- 到这里我们需要自定义的类已经弄好了,但是还没有加入到security管理流程中去,下面我们就把这些加入进去
-
- 扩展一个MyAuthenticationSecurityConfig配置类交给spring容器去管理
- 把刚才我们自定义的Filter 和provider加入到配置中去
- 然后我们需要在securi配置中加上扩展的配置
- 至此,整个配置流程就已经完成
- 启动项目测试
前言
一般程序的权限认证模块在登陆功能上,一般会扩展其他登陆方式,如短信登陆,扫码登录
这里我们主要讲一下怎么扩展短信认证登录
验证码存储我们采用redis作为缓存
(注意,这里为了测试方便改为手动设置验证码)
/ * @Auther: jiliugang * @Date: 2020/9/21 10:32 * @Description: */@RestController@RequestMapping("user")public class UserController { @Autowired private RedisTemplate redisTemplate; @RequestMapping("sms") public Result sms(String code, String mobile){ //这里需要判断手机号是否注册过,此处我省略了 Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent("sms:" + mobile, code, 1, TimeUnit.MINUTES); if (!aBoolean){ return ResultUtil.error(-1,"请求过于频繁"); }else { return ResultUtil.success(); } }}
经过上面接口,验证码已经存入到redis中,下面开始认证流程
自定义SmsCodeAuthenticationToken短信验证码认证token
##security默认登陆方式用的类为UsernamePasswordAuthenticationToken,我们可以以此为模板进行修改,变成我们想要的
下面是SmsCodeAuthenticationToken代码
/ * @Auther: jiliugang * @Date: 2020/9/18 13:54 * @Description: */public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = 530L; private final Object principal; private final String code; / * 认证成功前 * @param mobile */ public SmsCodeAuthenticationToken(String mobile,String code) { super(null); this.principal = mobile; this.code = code; super.setAuthenticated(false); } / * 认证成功后 * @param principal * @param authorities */ public SmsCodeAuthenticationToken(Object principal,Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; super.setAuthenticated(true); code = null; } @Override public Object getCredentials() { return null; } @Override public Object getPrincipal() { return this.principal; } @Override public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { if (isAuthenticated) { throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); } else { super.setAuthenticated(false); } } @Override public void eraseCredentials() { super.eraseCredentials(); } public String getCode() { return code; }}
有了sms认证token后,我们还要自定义登录过滤器SmsCodeAuthenticationFilter,专门用来处理短信登录路径
依然是参照security默认登录的过滤器UsernamePasswordAuthenticationFilter来实现自己的代码
/ * @Auther: jiliugang * @Date: 2020/9/18 14:02 * @Description: */public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter { private String mobileParam = "mobile"; private String mobileCode = "code"; private boolean postOnly = true; public SmsCodeAuthenticationFilter() { super(new AntPathRequestMatcher("/mobileAndCode/login", "POST")); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("不支持该类型请求: " + request.getMethod()); } else { String mobile = this.obtainMobile(request); String code = this.obtainCode(request); if (mobile == null) { mobile = ""; } if (code == null) { code = ""; } mobile = mobile.trim(); SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile,code); this.setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } } public void setMobileParameter(String mobileParameter) { Assert.hasText(mobileParameter, "mobile parameter must not be empty or null"); this.mobileParam = mobileParameter; } @Nullable protected String obtainMobile(HttpServletRequest request) { return request.getParameter(this.mobileParam); } @Nullable protected String obtainCode(HttpServletRequest request) { return request.getParameter(this.mobileCode); } protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) { authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); } public final String getMobileParameter() { return mobileParam; } public void setPostOnly(boolean postOnly) { this.postOnly = postOnly; }}
有了这两个后,我们还需要一个处理认证的provider,自定义一个SmsCodeAuthenticationProvider
依然是参照官方默认的DaoAuthenticationProvider,来实现我们自定义的
/ * @Auther: jiliugang * @Date: 2020/9/18 14:18 * @Description: */public class SmsCodeAuthenticationProvider implements AuthenticationProvider { protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); private UserDetailsService userDetailsService; private RedisTemplate redisTemplate = null; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.isInstanceOf(SmsCodeAuthenticationToken.class, authentication, () -> { return this.messages.getMessage("SmsCodeAuthenticationProvider.onlySupports", "Only SmsCodeAuthenticationToken is supported"); }); SmsCodeAuthenticationToken smsCodeAuthenticationToken = (SmsCodeAuthenticationToken) authentication; UserServiceImpl userService = null; if (UserServiceImpl.class.isInstance(userDetailsService)){ userService = (UserServiceImpl) userDetailsService; } String mobile = (String) authentication.getPrincipal(); String code = smsCodeAuthenticationToken.getCode(); Object o = redisTemplate.opsForValue().get("sms:" + mobile); if (o == null) { throw new InternalAuthenticationServiceException("找不到验证信息"); } if (!code.equals(o.toString())){ throw new BadCredentialsException("验证码错误"); } UserDetails loadedUser = userService.loadUserByMobileSms((String)authentication.getPrincipal(),smsCodeAuthenticationToken.getCode()); if (loadedUser == null) { throw new InternalAuthenticationServiceException("获取不到用户信息"); } SmsCodeAuthenticationToken result = new SmsCodeAuthenticationToken(loadedUser,loadedUser.getAuthorities()); result.setDetails(authentication.getDetails()); return result; } @Override public boolean supports(Class<?> authentication) { return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication); } public UserDetailsService getUserDetailsService() { return userDetailsService; } public void setUserDetailsService(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } public void setRedisTemplate(RedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; }}
到这里我们需要自定义的类已经弄好了,但是还没有加入到security管理流程中去,下面我们就把这些加入进去
扩展一个MyAuthenticationSecurityConfig配置类交给spring容器去管理
把刚才我们自定义的Filter 和provider加入到配置中去
/ * @Auther: jiliugang * @Date: 2020/9/18 16:20 * @Description: */@Componentpublic class MyAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { @Autowired private RedisTemplate redisTemplate; @Autowired private UserDetailsService userDetailsService; @Override public void configure(HttpSecurity http) throws Exception { SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter(); AuthenticationManager sharedObject = http.getSharedObject(AuthenticationManager.class); smsCodeAuthenticationFilter.setAuthenticationManager(sharedObject); smsCodeAuthenticationFilter.setAuthenticationFailureHandler(new SmsAuthenticationFailureHandler()); SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider(); smsCodeAuthenticationProvider.setRedisTemplate(redisTemplate); smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService); http.authenticationProvider(smsCodeAuthenticationProvider); http.addFilterBefore(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); }}
然后我们需要在securi配置中加上扩展的配置
/ * @Auther: jiliugang * @Date: 2020/9/4 15:59 * @Description: */@Configuration@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)public class WebSecurityConfig extends WebSecurityConfigurerAdapter { private AuthenticationManager authenticationManager; @Autowired private MyAuthenticationSecurityConfig myAuthenticationSecurityConfig; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { authenticationManager = super.authenticationManagerBean(); return authenticationManager; } @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/user/login.html","/mobileAndCode/login","/user/sms").permitAll() .antMatchers("/user/login","/login/","/.html").permitAll() .antMatchers("/css/", "/js/", "/plugins/","/static/","/imgs/","/*+.ico") .permitAll() .anyRequest().authenticated() .and().formLogin() .failureHandler(new FormAuthenticationFailureHandler()) .loginPage("/user/login.html") .loginProcessingUrl("/user/login") .and().apply(myAuthenticationSecurityConfig); }}
至此,整个配置流程就已经完成
启动项目测试
首先访问,获取验证码(注意,这里为了测试方便改为手动设置验证码)
登陆测试
访问
跳转登录
输入刚才的 账号 验证码
登陆成功!!!