SpringSecurity5(5-自定义短信、手机验证码)

莱莱  金牌会员 | 2025-3-14 11:38:34 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 979|帖子 979|积分 2937

图形验证码

SpringSecurity 实现的用户名、密码登录是在 UsernamePasswordAuthenticationFilter 过滤器进行认证的,而图形验证码一样平常是在用户名、密码认证之前进行验证的,所以须要在 UsernamePasswordAuthenticationFilter 过滤器之前添加一个自定义过滤器 ImageCodeValidateFilter,用来校验用户输入的图形验证码是否正确。
实现逻辑

自定义过滤器继承 OncePerRequestFilter 类,该类是 Spring 提供的在一次哀求中只会调用一次的 filter,确保每个哀求只会进入过滤器一次,避免了多次执行的情况
自定义的过滤器 ImageCodeValidateFilter 首先会判断哀求是否为 POST 方式的登录表单提交哀求,如果是就将其拦截进行图形验证码校验。如果验证错误,会抛出自定义异常类对象 ValidateCodeException,该异常类须要继承 AuthenticationException 类。在自定义过滤器中,我们须要手动捕获自定义异常类对象,并将捕获到自定义异常类对象交给自定义失败处理器进行处理

添加验证码配置
  1. <dependency>
  2.     <groupId>com.github.penggle</groupId>
  3.     <artifactId>kaptcha</artifactId>
  4.     <version>2.3.2</version>
  5. </dependency>
复制代码
  1. /**
  2. * 图形验证码的配置类
  3. */
  4. @Configuration
  5. public class KaptchaConfig {
  6.     @Bean
  7.     public DefaultKaptcha captchaProducer() {
  8.         DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
  9.         Properties properties = new Properties();
  10.         // 是否有边框
  11.         properties.setProperty(Constants.KAPTCHA_BORDER, "yes");
  12.         // 边框颜色
  13.         properties.setProperty(Constants.KAPTCHA_BORDER_COLOR, "192,192,192");
  14.         // 验证码图片的宽和高
  15.         properties.setProperty(Constants.KAPTCHA_IMAGE_WIDTH, "110");
  16.         properties.setProperty(Constants.KAPTCHA_IMAGE_HEIGHT, "40");
  17.         // 验证码颜色
  18.         properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_COLOR, "0,0,0");
  19.         // 验证码字体大小
  20.         properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_SIZE, "32");
  21.         // 验证码生成几个字符
  22.         properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
  23.         // 验证码随机字符库
  24.         properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_STRING, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYAZ");
  25.         // 验证码图片默认是有线条干扰的,我们设置成没有干扰
  26.         properties.setProperty(Constants.KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");
  27.         Config config = new Config(properties);
  28.         defaultKaptcha.setConfig(config);
  29.         return defaultKaptcha;
  30.     }
  31. }
复制代码
提供验证码接口
  1. public class CheckCode implements Serializable {
  2.     private String code;           // 验证码字符
  3.     private LocalDateTime expireTime;  // 过期时间
  4.     /**
  5.      * @param code 验证码字符
  6.      * @param expireTime 过期时间,单位秒
  7.      */
  8.     public CheckCode(String code, int expireTime) {
  9.         this.code = code;
  10.         this.expireTime = LocalDateTime.now().plusSeconds(expireTime);
  11.     }
  12.     public CheckCode(String code) {
  13.         // 默认验证码 60 秒后过期
  14.         this(code, 60);
  15.     }
  16.     // 是否过期
  17.     public boolean isExpried() {
  18.         return this.expireTime.isBefore(LocalDateTime.now());
  19.     }
  20.     public String getCode() {
  21.         return this.code;
  22.     }
  23. }
复制代码
  1. @Controller
  2. public class LoginController {
  3.     // Session 中存储图形验证码的属性名
  4.     public static final String KAPTCHA_SESSION_KEY = "KAPTCHA_SESSION_KEY";
  5.     @Autowired
  6.     private DefaultKaptcha defaultKaptcha;
  7.     @GetMapping("/login/page")
  8.     public String login() {
  9.         return "login";
  10.     }
  11.     @GetMapping("/code/image")
  12.     public void imageCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
  13.         // 创建验证码文本
  14.         String capText = defaultKaptcha.createText();
  15.         // 创建验证码图片
  16.         BufferedImage image = defaultKaptcha.createImage(capText);
  17.         // 将验证码文本放进 Session 中
  18.         CheckCode code = new CheckCode(capText);
  19.         request.getSession().setAttribute(KAPTCHA_SESSION_KEY, code);
  20.         // 将验证码图片返回,禁止验证码图片缓存
  21.         response.setHeader("Cache-Control", "no-store");
  22.         response.setHeader("Pragma", "no-cache");
  23.         response.setDateHeader("Expires", 0);
  24.         response.setContentType("image/jpeg");
  25.         ImageIO.write(image, "jpg", response.getOutputStream());
  26.     }   
  27. }
复制代码
  1. <!DOCTYPE html>
  2. <html lang="en" xmlns:th="http://www.thymeleaf.org">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <title>登录</title>
  6. </head>
  7. <body>
  8.     <h3>表单登录</h3>
  9.     <form method="post" th:action="@{/login/form}">
  10.         <input type="text" name="username" placeholder="用户名">
  11.         <input type="password" name="password" placeholder="密码">
  12.         <input name="imageCode" type="text" placeholder="验证码">
  13.         <img th:onclick="this.src='/code/image?'+Math.random()" th:src="@{/code/image}" alt="验证码"/>
  14.         
  15.             用户名或密码错误
  16.         
  17.         <button type="submit">登录</button>
  18.     </form>
  19. </body>
  20. </html>
复制代码
自定义验证码过滤器
  1. /**
  2. * 自定义验证码校验错误的异常类,继承 AuthenticationException
  3. */
  4. public class ValidateCodeException extends AuthenticationException {
  5.     public ValidateCodeException(String msg, Throwable t) {
  6.         super(msg, t);
  7.     }
  8.     public ValidateCodeException(String msg) {
  9.         super(msg);
  10.     }
  11. }
复制代码
  1. @Component
  2. public class ImageCodeValidateFilter extends OncePerRequestFilter {
  3.     private String codeParamter = "imageCode";  // 前端输入的图形验证码参数名
  4.     @Autowired
  5.     private AuthenticationFailureHandlerImpl authenticationFailureHandler;  // 自定义认证失败处理器
  6.     @Override
  7.     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
  8.         // 非 POST 方式的表单提交请求不校验图形验证码
  9.         if ("/login/form".equals(request.getRequestURI()) && "POST".equals(request.getMethod())) {
  10.             try {
  11.                 // 校验图形验证码合法性
  12.                 validate(request);
  13.             } catch (ValidateCodeException e) {
  14.                 // 手动捕获图形验证码校验过程抛出的异常,将其传给失败处理器进行处理
  15.                 authenticationFailureHandler.onAuthenticationFailure(request, response, e);
  16.                 return;
  17.             }
  18.         }
  19.         // 放行请求,进入下一个过滤器
  20.         filterChain.doFilter(request, response);
  21.     }
  22.     // 判断验证码的合法性
  23.     private void validate(HttpServletRequest request) {
  24.         // 获取用户传入的图形验证码值
  25.         String requestCode = request.getParameter(this.codeParamter);
  26.         if(requestCode == null) {
  27.             requestCode = "";
  28.         }
  29.         requestCode = requestCode.trim();
  30.         // 获取 Session
  31.         HttpSession session = request.getSession();
  32.         // 获取存储在 Session 里的验证码值
  33.         CheckCode savedCode = (CheckCode) session.getAttribute(LoginController.KAPTCHA_SESSION_KEY);
  34.         if (savedCode != null) {
  35.             // 随手清除验证码,无论是失败,还是成功。客户端应在登录失败时刷新验证码
  36.             session.removeAttribute(LoginController.KAPTCHA_SESSION_KEY);
  37.         }
  38.         // 校验出错,抛出异常
  39.         if (StringUtils.isBlank(requestCode)) {
  40.             throw new ValidateCodeException("验证码的值不能为空");
  41.         }
  42.         if (savedCode == null) {
  43.             throw new ValidateCodeException("验证码不存在");
  44.         }
  45.         if (savedCode.isExpried()) {
  46.             throw new ValidateCodeException("验证码过期");
  47.         }
  48.         if (!requestCode.equalsIgnoreCase(savedCode.getCode())) {
  49.             throw new ValidateCodeException("验证码输入错误");
  50.         }
  51.     }
  52. }
复制代码
  1. @Component
  2. public class UserDetailServiceImpl implements UserDetailsService {
  3.     @Autowired
  4.     private PasswordEncoder passwordEncoder;
  5.     @Override
  6.     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  7.         if ("root".equals(username)) {
  8.             return new User(username, passwordEncoder.encode("123"), AuthorityUtils.createAuthorityList("admin"));
  9.         } else {
  10.             throw new UsernameNotFoundException("用户名不存在");
  11.         }
  12.     }
  13. }
复制代码
设置过滤器次序
  1. @Configuration
  2. public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
  3.     @Autowired
  4.     private AuthenticationSuccessHandlerImpl authenticationSuccessHandler;
  5.     @Autowired
  6.     private AuthenticationFailureHandlerImpl authenticationFailureHandler;
  7.     @Autowired
  8.     private ImageCodeValidateFilter imageCodeValidateFilter; // 自定义过滤器(图形验证码校验)
  9.     @Autowired
  10.     private UserDetailServiceImpl userDetailService;
  11.     /**
  12.      * 密码编码器,密码不能明文存储
  13.      */
  14.     @Bean
  15.     public BCryptPasswordEncoder passwordEncoder() {
  16.         // 使用 BCryptPasswordEncoder 密码编码器,该编码器会将随机产生的 salt 混入最终生成的密文中
  17.         return new BCryptPasswordEncoder();
  18.     }
  19.     @Override
  20.     protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  21.         auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());
  22.     }
  23.     /**
  24.      * 定制基于 HTTP 请求的用户访问控制
  25.      */
  26.     @Override
  27.     protected void configure(HttpSecurity http) throws Exception {
  28.         // 启动 form 表单登录
  29.         http.formLogin()
  30.             // 设置登录页面的访问路径,默认为 /login,GET 请求;该路径不设限访问
  31.             .loginPage("/login/page")
  32.             // 设置登录表单提交路径,默认为 loginPage() 设置的路径,POST 请求
  33.             .loginProcessingUrl("/login/form")
  34.             // 使用自定义的认证成功和失败处理器
  35.             .successHandler(authenticationSuccessHandler)
  36.             .failureHandler(authenticationFailureHandler);
  37.         // 开启基于 HTTP 请求访问控制
  38.         http.authorizeRequests()
  39.             // 以下访问不需要任何权限,任何人都可以访问
  40.             .antMatchers("/login/page", "/code/image").permitAll()
  41.             // 其它任何请求访问都需要先通过认证
  42.             .anyRequest().authenticated();
  43.         // 关闭 csrf 防护
  44.         http.csrf().disable();
  45.         // 将自定义过滤器(图形验证码校验)添加到 UsernamePasswordAuthenticationFilter 之前
  46.         http.addFilterBefore(imageCodeValidateFilter, UsernamePasswordAuthenticationFilter.class);        
  47.     }
  48.     /**
  49.      * 定制一些全局性的安全配置,例如:不拦截静态资源的访问
  50.      */
  51.     @Override
  52.     public void configure(WebSecurity web) throws Exception {
  53.         // 静态资源的访问不需要拦截,直接放行
  54.         web.ignoring().antMatchers("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
  55.     }
  56. }
复制代码
手机短信验证码

验证流程

带有图形验证码的用户名、密码登录流程:

  • 在 ImageCodeValidateFilter 过滤器中校验用户输入的图形验证码是否正确。
  • 在 UsernamePasswordAuthenticationFilter 过滤器中将 username 和 password 天生一个用于认证的 Token(UsernamePasswordAuthenticationToken),并将其传递给 ProviderManager 接口的实现类 AuthenticationManager。
  • AuthenticationManager 管理器寻找到一个合适的处理器 DaoAuthenticationProvider 来处理 UsernamePasswordAuthenticationToken。
  • DaoAuthenticationProvider 通过 UserDetailsService 接口的实现类 CustomUserDetailsService 从数据库中获取指定 username 的相关信息,并校验用户输入的 password。如果校验乐成,那就认证通过,用户信息类对象 Authentication 标志为已认证。
  • 认证通过后,将已认证的用户信息对象 Authentication 存储到 SecurityContextHolder 中,最终存储到 Session 中。
仿照上述流程,我们分析手机短信验证码登录流程:

  • 仿照 ImageCodeValidateFilter 过滤器设计 MobileVablidateFilter 过滤器,该过滤器用来校验用户输入手机短信验证码。
  • 仿照 UsernamePasswordAuthenticationFilter 过滤器设计 MobileAuthenticationFilter 过滤器,该过滤器将用户输入的手机号天生一个 Token(MobileAuthenticationToken),并将其传递给 ProviderManager 接口的实现类 AuthenticationManager。
  • AuthenticationManager 管理器寻找到一个合适的处理器 MobileAuthenticationProvider 来处理 MobileAuthenticationToken,该处理器是仿照 DaoAuthenticationProvider 进行设计的。
  • MobileAuthenticationProvider 通过 UserDetailsService 接口的实现类 MobileUserDetailsService 从数据库中获取指定手机号对应的用户信息,此处不须要进行任何校验,直接将用户信息类对象 Authentication 标志为已认证。
  • 认证通过后,将已认证的用户信息对象 Authentication 存储到 SecurityContextHolder 中,最终存储到 Session 中,此处的操作不须要我们编写。
末了通过自定义配置类 MobileAuthenticationConfig 组合上述组件,并添加到安全配置类 SpringSecurityConfig 中。

提供短信发送接口
  1. @Controller
  2. public class LoginController {
  3.     // Session 中存储手机短信验证码的属性名
  4.     public static final String MOBILE_SESSION_KEY = "MOBILE_SESSION_KEY";
  5.     @GetMapping("/mobile/page")
  6.     public String mobileLoginPage() {  // 跳转到手机短信验证码登录页面
  7.         return "login-mobile";
  8.     }
  9.     @GetMapping("/code/mobile")
  10.     @ResponseBody
  11.     public Object sendMoblieCode(HttpServletRequest request) {
  12.         // 随机生成一个 4 位的验证码
  13.         String code = RandomStringUtils.randomNumeric(4);
  14.         // 将手机验证码文本存储在 Session 中,设置过期时间为 10 * 60s
  15.         CheckCode mobileCode = new CheckCode(code, 10 * 60);
  16.         request.getSession().setAttribute(MOBILE_SESSION_KEY, mobileCode);
  17.         return mobileCode;
  18.     }   
  19. }
复制代码
  1. <!DOCTYPE html>
  2. <html lang="en" xmlns:th="http://www.thymeleaf.org">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <title>登录页面</title>
  6.    
  7. </head>
  8. <body>
  9.     <form method="post" th:action="@{/mobile/form}">
  10.         <input id="mobile" name="mobile" type="text" placeholder="手机号码">
  11.         
  12.             <input name="mobileCode" type="text" placeholder="验证码">
  13.             <button type="button" id="sendCode">获取验证码</button>
  14.         
  15.         
  16.             用户名或密码错误
  17.         
  18.         <button type="submit">登录</button>
  19.     </form>
  20.    
  21. </body>
  22. </html>
复制代码
自定义短信验证码校验过滤器

更改自定义失败处理器 CustomAuthenticationFailureHandler,原先的处理器在认证失败时,会直接重定向到/login/page?error 显示认证异常信息。现在我们有两种登录方式,应该进行以下处理:

  • 带图形验证码的用户名、密码方式登录方式出现认证异常,重定向到/login/page?error
  • 手机短信验证码方式登录出现认证异常,重定向到/mobile/page?error
  1. /**
  2. * 继承 SimpleUrlAuthenticationFailureHandler 处理器,该类是 failureUrl() 方法使用的认证失败处理器
  3. */
  4. @Component
  5. public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
  6.     @Override
  7.     public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
  8.         String xRequestedWith = request.getHeader("x-requested-with");
  9.         // 判断前端的请求是否为 ajax 请求
  10.         if ("XMLHttpRequest".equals(xRequestedWith)) {
  11.             // 认证成功,响应 JSON 数据
  12.             response.setContentType("application/json;charset=utf-8");
  13.             response.getWriter().write("认证失败");
  14.         }else {
  15.             // 用户名、密码方式登录出现认证异常,需要重定向到 /login/page?error
  16.             // 手机短信验证码方式登录出现认证异常,需要重定向到 /mobile/page?error
  17.             // 使用 Referer 获取当前登录表单提交请求是从哪个登录页面(/login/page 或 /mobile/page)链接过来的
  18.             String refer = request.getHeader("Referer");
  19.             String lastUrl = StringUtils.substringBefore(refer, "?");
  20.             // 设置默认的重定向路径
  21.             super.setDefaultFailureUrl(lastUrl + "?error");
  22.             // 调用父类的 onAuthenticationFailure() 方法
  23.             super.onAuthenticationFailure(request, response, e);
  24.         }
  25.     }
  26. }
复制代码
  1. /**
  2. * 手机短信验证码校验
  3. */
  4. @Component
  5. public class MobileCodeValidateFilter extends OncePerRequestFilter {
  6.     private String codeParamter = "mobileCode";  // 前端输入的手机短信验证码参数名
  7.     @Autowired
  8.     private CustomAuthenticationFailureHandler authenticationFailureHandler; // 自定义认证失败处理器
  9.     @Override
  10.     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
  11.         // 非 POST 方式的手机短信验证码提交请求不进行校验
  12.         if("/mobile/form".equals(request.getRequestURI()) && "POST".equals(request.getMethod())) {
  13.             try {
  14.                 // 检验手机验证码的合法性
  15.                 validate(request);
  16.             } catch (ValidateCodeException e) {
  17.                 // 将异常交给自定义失败处理器进行处理
  18.                 authenticationFailureHandler.onAuthenticationFailure(request, response, e);
  19.                 return;
  20.             }
  21.         }
  22.         // 放行,进入下一个过滤器
  23.         filterChain.doFilter(request, response);
  24.     }
  25.     /**
  26.      * 检验用户输入的手机验证码的合法性
  27.      */
  28.     private void validate(HttpServletRequest request) {
  29.         // 获取用户传入的手机验证码值
  30.         String requestCode = request.getParameter(this.codeParamter);
  31.         if(requestCode == null) {
  32.             requestCode = "";
  33.         }
  34.         requestCode = requestCode.trim();
  35.         // 获取 Session
  36.         HttpSession session = request.getSession();
  37.         // 获取 Session 中存储的手机短信验证码
  38.         CheckCode savedCode = (CheckCode) session.getAttribute(LoginController.MOBILE_SESSION_KEY);
  39.         if (savedCode != null) {
  40.             // 随手清除验证码,无论是失败,还是成功。客户端应在登录失败时刷新验证码
  41.             session.removeAttribute(LoginController.MOBILE_SESSION_KEY);
  42.         }
  43.         // 校验出错,抛出异常
  44.         if (StringUtils.isBlank(requestCode)) {
  45.             throw new ValidateCodeException("验证码的值不能为空");
  46.         }
  47.         if (savedCode == null) {
  48.             throw new ValidateCodeException("验证码不存在");
  49.         }
  50.         if (savedCode.isExpried()) {
  51.             throw new ValidateCodeException("验证码过期");
  52.         }
  53.         if (!requestCode.equalsIgnoreCase(savedCode.getCode())) {
  54.             throw new ValidateCodeException("验证码输入错误");
  55.         }
  56.     }
  57. }
复制代码
自定义短信验证码认证过滤器


  • 仿照 UsernamePasswordAuthenticationToken 类进行编写
  • 仿照 UsernamePasswordAuthenticationFilter 过滤器进行编写
  1. public class MobileAuthenticationToken extends AbstractAuthenticationToken {
  2.     private static final long serialVersionUID = 520L;
  3.     private final Object principal;
  4.     /**
  5.      * 认证前,使用该构造器进行封装信息
  6.      */
  7.     public MobileAuthenticationToken(Object principal) {
  8.         super(null);     // 用户权限为 null
  9.         this.principal = principal;   // 前端传入的手机号
  10.         this.setAuthenticated(false); // 标记为未认证
  11.     }
  12.     /**
  13.      * 认证成功后,使用该构造器封装用户信息
  14.      */
  15.     public MobileAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
  16.         super(authorities);          // 用户权限集合
  17.         this.principal = principal;  // 封装认证用户信息的 UserDetails 对象,不再是手机号
  18.         super.setAuthenticated(true); // 标记认证成功
  19.     }
  20.     @Override
  21.     public Object getCredentials() {
  22.         // 由于使用手机短信验证码登录不需要密码,所以直接返回 null
  23.         return null;
  24.     }
  25.     @Override
  26.     public Object getPrincipal() {
  27.         return this.principal;
  28.     }
  29.     @Override
  30.     public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
  31.         if (isAuthenticated) {
  32.             throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
  33.         } else {
  34.             super.setAuthenticated(false);
  35.         }
  36.     }
  37.     @Override
  38.     public void eraseCredentials() {
  39.         // 手机短信验证码认证方式不必去除额外的敏感信息,所以直接调用父类方法
  40.         super.eraseCredentials();
  41.     }
  42. }
复制代码
  1. /**
  2. * 手机短信验证码认证过滤器,仿照 UsernamePasswordAuthenticationFilter 过滤器编写
  3. */
  4. public class MobileAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
  5.     private String mobileParamter = "mobile";  // 默认手机号参数名为 mobile
  6.     private boolean postOnly = true;    // 默认请求方式只能为 POST
  7.     protected MobileAuthenticationFilter() {
  8.         // 默认登录表单提交路径为 /mobile/form,POST 方式请求
  9.         super(new AntPathRequestMatcher("/mobile/form", "POST"));
  10.     }
  11.     @Override
  12.     public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
  13.         //(1) 默认情况下,如果请求方式不是 POST,会抛出异常
  14.         if(postOnly && !request.getMethod().equals("POST")) {
  15.             throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
  16.         }else {
  17.             //(2) 获取请求携带的 mobile
  18.             String mobile = request.getParameter(mobileParamter);
  19.             if(mobile == null) {
  20.                 mobile = "";
  21.             }
  22.             mobile = mobile.trim();
  23.             //(3) 使用前端传入的 mobile 构造 Authentication 对象,标记该对象未认证
  24.             // MobileAuthenticationToken 是我们自定义的 Authentication 类,后续介绍
  25.             MobileAuthenticationToken authRequest = new MobileAuthenticationToken(mobile);
  26.             //(4) 将请求中的一些属性信息设置到 Authentication 对象中,如:remoteAddress,sessionId
  27.             this.setDetails(request, authRequest);
  28.             //(5) 调用 ProviderManager 类的 authenticate() 方法进行身份认证
  29.             return this.getAuthenticationManager().authenticate(authRequest);
  30.         }
  31.     }
  32.     @Nullable
  33.     protected String obtainMobile(HttpServletRequest request) {
  34.         return request.getParameter(this.mobileParamter);
  35.     }
  36.     protected void setDetails(HttpServletRequest request, MobileAuthenticationToken authRequest) {
  37.         authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
  38.     }
  39.     public void setMobileParameter(String mobileParamter) {
  40.         Assert.hasText(mobileParamter, "Mobile par ameter must not be empty or null");
  41.         this.mobileParamter = mobileParamter;
  42.     }
  43.     public void setPostOnly(boolean postOnly) {
  44.         this.postOnly = postOnly;
  45.     }
  46.     public String getMobileParameter() {
  47.         return mobileParamter;
  48.     }
  49. }
复制代码
自定义短信验证码认证方式配置类


  • 将上述组件进行管理,仿照 SecurityConfigurerAdapter类进行编写
  • 绑定到最终的安全配置类 SpringSecurityConfig 中
  1. public class MobileAuthenticationProvider implements AuthenticationProvider {
  2.     private UserDetailsService userDetailsService;
  3.     protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
  4.     private UserDetailsChecker authenticationChecks = new MobileAuthenticationProvider.DefaultAuthenticationChecks();
  5.     /**
  6.      * 处理认证
  7.      */
  8.     @Override
  9.     public Authentication authenticate(Authentication authentication) throws AuthenticationException {
  10.         //(1) 如果入参的 Authentication 类型不是 MobileAuthenticationToken,抛出异常
  11.         Assert.isInstanceOf(MobileAuthenticationToken.class, authentication, () -> {
  12.             return this.messages.getMessage("MobileAuthenticationProvider.onlySupports", "Only MobileAuthenticationToken is supported");
  13.         });
  14.         // 获取手机号
  15.         String mobile = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
  16.         //(2) 根据手机号从数据库中查询用户信息
  17.         UserDetails user = this.userDetailsService.loadUserByUsername(mobile);
  18.         if (user == null) {
  19.             //(3) 未查询到用户信息,抛出异常
  20.             throw new AuthenticationServiceException("该手机号未注册");
  21.         }
  22.         //(4) 检查账号是否锁定、账号是否可用、账号是否过期、密码是否过期
  23.         this.authenticationChecks.check(user);
  24.         //(5) 查询到了用户信息,则认证通过,构建标记认证成功用户信息类对象 AuthenticationToken
  25.         MobileAuthenticationToken result = new MobileAuthenticationToken(user, user.getAuthorities());
  26.         // 需要把认证前 Authentication 对象中的 details 信息加入认证后的 Authentication
  27.         result.setDetails(authentication.getDetails());
  28.         return result;
  29.     }
  30.     /**
  31.      * ProviderManager 管理器通过此方法来判断是否采用此 AuthenticationProvider 类
  32.      * 来处理由 AuthenticationFilter 过滤器传入的 Authentication 对象
  33.      */
  34.     @Override
  35.     public boolean supports(Class<?> authentication) {
  36.         // isAssignableFrom 返回 true 当且仅当调用者为父类.class,参数为本身或者其子类.class
  37.         // ProviderManager 会获取 MobileAuthenticationFilter 过滤器传入的 Authentication 类型
  38.         // 所以当且仅当 authentication 的类型为 MobileAuthenticationToken 才返回 true
  39.         return MobileAuthenticationToken.class.isAssignableFrom(authentication);
  40.     }
  41.     /**
  42.      * 此处传入自定义的 MobileUserDetailsSevice 对象
  43.      */
  44.     public void setUserDetailsService(UserDetailsService userDetailsService) {
  45.         this.userDetailsService = userDetailsService;
  46.     }
  47.     public UserDetailsService getUserDetailsService() {
  48.         return userDetailsService;
  49.     }
  50.     /**
  51.      * 检查账号是否锁定、账号是否可用、账号是否过期、密码是否过期
  52.      */
  53.     private class DefaultAuthenticationChecks implements UserDetailsChecker {
  54.         private DefaultAuthenticationChecks() {
  55.         }
  56.         @Override
  57.         public void check(UserDetails user) {
  58.             if (!user.isAccountNonLocked()) {
  59.                 throw new LockedException(MobileAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"));
  60.             } else if (!user.isEnabled()) {
  61.                 throw new DisabledException(MobileAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"));
  62.             } else if (!user.isAccountNonExpired()) {
  63.                 throw new AccountExpiredException(MobileAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"));
  64.             } else if (!user.isCredentialsNonExpired()) {
  65.                 throw new CredentialsExpiredException(MobileAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.credentialsExpired", "User credentials have expired"));
  66.             }
  67.         }
  68.     }
  69. }
复制代码
  1. @Service
  2. public class MobileUserDetailsService implements UserDetailsService {
  3.     @Autowired
  4.     private UserMapper userMapper;
  5.     @Override
  6.     public UserDetails loadUserByUsername(String mobile) throws UsernameNotFoundException {
  7.         //(1) 从数据库尝试读取该用户
  8.         User user = userMapper.selectByMobile(mobile);
  9.         // 用户不存在,抛出异常
  10.         if (user == null) {
  11.             throw new UsernameNotFoundException("用户不存在");
  12.         }
  13.         //(2) 将数据库形式的 roles 解析为 UserDetails 的权限集合
  14.         // AuthorityUtils.commaSeparatedStringToAuthorityList() 是 Spring Security 提供的方法,用于将逗号隔开的权限集字符串切割为可用权限对象列表
  15.         user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));
  16.         //(3) 返回 UserDetails 对象
  17.         return user;
  18.     }
  19. }
复制代码


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

莱莱

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表