> 文档中心 > springsecurity自定义短信验证码认证登录流程

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);    }}

至此,整个配置流程就已经完成

启动项目测试

首先访问,获取验证码(注意,这里为了测试方便改为手动设置验证码)
在这里插入图片描述

登陆测试

访问
在这里插入图片描述
跳转登录

在这里插入图片描述
输入刚才的 账号 验证码
登陆成功!!!