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

news/2025/3/14 11:29:05/文章来源:https://www.cnblogs.com/penggx/p/18771560

图形验证码

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

实现逻辑

自定义过滤器继承 OncePerRequestFilter 类,该类是 Spring 提供的在一次请求中只会调用一次的 filter,确保每个请求只会进入过滤器一次,避免了多次执行的情况

自定义的过滤器 ImageCodeValidateFilter 首先会判断请求是否为 POST 方式的登录表单提交请求,如果是就将其拦截进行图形验证码校验。如果验证错误,会抛出自定义异常类对象 ValidateCodeException,该异常类需要继承 AuthenticationException 类。在自定义过滤器中,我们需要手动捕获自定义异常类对象,并将捕获到自定义异常类对象交给自定义失败处理器进行处理

添加验证码配置

<!-- 图形验证码工具 kaptcha -->
<dependency><groupId>com.github.penggle</groupId><artifactId>kaptcha</artifactId><version>2.3.2</version>
</dependency>
/*** 图形验证码的配置类*/
@Configuration
public class KaptchaConfig {@Beanpublic DefaultKaptcha captchaProducer() {DefaultKaptcha defaultKaptcha = new DefaultKaptcha();Properties properties = new Properties();// 是否有边框properties.setProperty(Constants.KAPTCHA_BORDER, "yes");// 边框颜色properties.setProperty(Constants.KAPTCHA_BORDER_COLOR, "192,192,192");// 验证码图片的宽和高properties.setProperty(Constants.KAPTCHA_IMAGE_WIDTH, "110");properties.setProperty(Constants.KAPTCHA_IMAGE_HEIGHT, "40");// 验证码颜色properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_COLOR, "0,0,0");// 验证码字体大小properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_SIZE, "32");// 验证码生成几个字符properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");// 验证码随机字符库properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_STRING, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYAZ");// 验证码图片默认是有线条干扰的,我们设置成没有干扰properties.setProperty(Constants.KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");Config config = new Config(properties);defaultKaptcha.setConfig(config);return defaultKaptcha;}
}

提供验证码接口

public class CheckCode implements Serializable {private String code;           // 验证码字符private LocalDateTime expireTime;  // 过期时间/*** @param code 验证码字符* @param expireTime 过期时间,单位秒*/public CheckCode(String code, int expireTime) {this.code = code;this.expireTime = LocalDateTime.now().plusSeconds(expireTime);}public CheckCode(String code) {// 默认验证码 60 秒后过期this(code, 60);}// 是否过期public boolean isExpried() {return this.expireTime.isBefore(LocalDateTime.now());}public String getCode() {return this.code;}
}
@Controller
public class LoginController {// Session 中存储图形验证码的属性名public static final String KAPTCHA_SESSION_KEY = "KAPTCHA_SESSION_KEY";@Autowiredprivate DefaultKaptcha defaultKaptcha;@GetMapping("/login/page")public String login() {return "login";}@GetMapping("/code/image")public void imageCode(HttpServletRequest request, HttpServletResponse response) throws IOException {// 创建验证码文本String capText = defaultKaptcha.createText();// 创建验证码图片BufferedImage image = defaultKaptcha.createImage(capText);// 将验证码文本放进 Session 中CheckCode code = new CheckCode(capText);request.getSession().setAttribute(KAPTCHA_SESSION_KEY, code);// 将验证码图片返回,禁止验证码图片缓存response.setHeader("Cache-Control", "no-store");response.setHeader("Pragma", "no-cache");response.setDateHeader("Expires", 0);response.setContentType("image/jpeg");ImageIO.write(image, "jpg", response.getOutputStream());}    
}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>登录</title>
</head>
<body><h3>表单登录</h3><form method="post" th:action="@{/login/form}"><input type="text" name="username" placeholder="用户名"><br><input type="password" name="password" placeholder="密码"><br><input name="imageCode" type="text" placeholder="验证码"><br><img th:onclick="this.src='/code/image?'+Math.random()" th:src="@{/code/image}" alt="验证码"/><br><div th:if="${param.error}"><span th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message}" style="color:red">用户名或密码错误</span></div><button type="submit">登录</button></form>
</body>
</html>

自定义验证码过滤器

/*** 自定义验证码校验错误的异常类,继承 AuthenticationException*/
public class ValidateCodeException extends AuthenticationException {public ValidateCodeException(String msg, Throwable t) {super(msg, t);}public ValidateCodeException(String msg) {super(msg);}
}
@Component
public class ImageCodeValidateFilter extends OncePerRequestFilter {private String codeParamter = "imageCode";  // 前端输入的图形验证码参数名@Autowiredprivate AuthenticationFailureHandlerImpl authenticationFailureHandler;  // 自定义认证失败处理器@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {// 非 POST 方式的表单提交请求不校验图形验证码if ("/login/form".equals(request.getRequestURI()) && "POST".equals(request.getMethod())) {try {// 校验图形验证码合法性validate(request);} catch (ValidateCodeException e) {// 手动捕获图形验证码校验过程抛出的异常,将其传给失败处理器进行处理authenticationFailureHandler.onAuthenticationFailure(request, response, e);return;}}// 放行请求,进入下一个过滤器filterChain.doFilter(request, response);}// 判断验证码的合法性private void validate(HttpServletRequest request) {// 获取用户传入的图形验证码值String requestCode = request.getParameter(this.codeParamter);if(requestCode == null) {requestCode = "";}requestCode = requestCode.trim();// 获取 SessionHttpSession session = request.getSession();// 获取存储在 Session 里的验证码值CheckCode savedCode = (CheckCode) session.getAttribute(LoginController.KAPTCHA_SESSION_KEY);if (savedCode != null) {// 随手清除验证码,无论是失败,还是成功。客户端应在登录失败时刷新验证码session.removeAttribute(LoginController.KAPTCHA_SESSION_KEY);}// 校验出错,抛出异常if (StringUtils.isBlank(requestCode)) {throw new ValidateCodeException("验证码的值不能为空");}if (savedCode == null) {throw new ValidateCodeException("验证码不存在");}if (savedCode.isExpried()) {throw new ValidateCodeException("验证码过期");}if (!requestCode.equalsIgnoreCase(savedCode.getCode())) {throw new ValidateCodeException("验证码输入错误");}}
}
@Component
public class UserDetailServiceImpl implements UserDetailsService {@Autowiredprivate PasswordEncoder passwordEncoder;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {if ("root".equals(username)) {return new User(username, passwordEncoder.encode("123"), AuthorityUtils.createAuthorityList("admin"));} else {throw new UsernameNotFoundException("用户名不存在");}}
}

设置过滤器顺序

@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate AuthenticationSuccessHandlerImpl authenticationSuccessHandler;@Autowiredprivate AuthenticationFailureHandlerImpl authenticationFailureHandler;@Autowiredprivate ImageCodeValidateFilter imageCodeValidateFilter; // 自定义过滤器(图形验证码校验)@Autowiredprivate UserDetailServiceImpl userDetailService;/*** 密码编码器,密码不能明文存储*/@Beanpublic BCryptPasswordEncoder passwordEncoder() {// 使用 BCryptPasswordEncoder 密码编码器,该编码器会将随机产生的 salt 混入最终生成的密文中return new BCryptPasswordEncoder();}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());}/*** 定制基于 HTTP 请求的用户访问控制*/@Overrideprotected void configure(HttpSecurity http) throws Exception {// 启动 form 表单登录http.formLogin()// 设置登录页面的访问路径,默认为 /login,GET 请求;该路径不设限访问.loginPage("/login/page")// 设置登录表单提交路径,默认为 loginPage() 设置的路径,POST 请求.loginProcessingUrl("/login/form")// 使用自定义的认证成功和失败处理器.successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler);// 开启基于 HTTP 请求访问控制http.authorizeRequests()// 以下访问不需要任何权限,任何人都可以访问.antMatchers("/login/page", "/code/image").permitAll()// 其它任何请求访问都需要先通过认证.anyRequest().authenticated();// 关闭 csrf 防护http.csrf().disable();// 将自定义过滤器(图形验证码校验)添加到 UsernamePasswordAuthenticationFilter 之前http.addFilterBefore(imageCodeValidateFilter, UsernamePasswordAuthenticationFilter.class);        }/*** 定制一些全局性的安全配置,例如:不拦截静态资源的访问*/@Overridepublic void configure(WebSecurity web) throws Exception {// 静态资源的访问不需要拦截,直接放行web.ignoring().antMatchers("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");}
}

手机短信验证码

验证流程

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

  1. 在 ImageCodeValidateFilter 过滤器中校验用户输入的图形验证码是否正确。
  2. 在 UsernamePasswordAuthenticationFilter 过滤器中将 username 和 password 生成一个用于认证的 Token(UsernamePasswordAuthenticationToken),并将其传递给 ProviderManager 接口的实现类 AuthenticationManager。
  3. AuthenticationManager 管理器寻找到一个合适的处理器 DaoAuthenticationProvider 来处理 UsernamePasswordAuthenticationToken。
  4. DaoAuthenticationProvider 通过 UserDetailsService 接口的实现类 CustomUserDetailsService 从数据库中获取指定 username 的相关信息,并校验用户输入的 password。如果校验成功,那就认证通过,用户信息类对象 Authentication 标记为已认证。
  5. 认证通过后,将已认证的用户信息对象 Authentication 存储到 SecurityContextHolder 中,最终存储到 Session 中。

仿照上述流程,我们分析手机短信验证码登录流程:

  1. 仿照 ImageCodeValidateFilter 过滤器设计 MobileVablidateFilter 过滤器,该过滤器用来校验用户输入手机短信验证码。
  2. 仿照 UsernamePasswordAuthenticationFilter 过滤器设计 MobileAuthenticationFilter 过滤器,该过滤器将用户输入的手机号生成一个 Token(MobileAuthenticationToken),并将其传递给 ProviderManager 接口的实现类 AuthenticationManager。
  3. AuthenticationManager 管理器寻找到一个合适的处理器 MobileAuthenticationProvider 来处理 MobileAuthenticationToken,该处理器是仿照 DaoAuthenticationProvider 进行设计的。
  4. MobileAuthenticationProvider 通过 UserDetailsService 接口的实现类 MobileUserDetailsService 从数据库中获取指定手机号对应的用户信息,此处不需要进行任何校验,直接将用户信息类对象 Authentication 标记为已认证。
  5. 认证通过后,将已认证的用户信息对象 Authentication 存储到 SecurityContextHolder 中,最终存储到 Session 中,此处的操作不需要我们编写。

最后通过自定义配置类 MobileAuthenticationConfig 组合上述组件,并添加到安全配置类 SpringSecurityConfig 中。

提供短信发送接口

@Controller
public class LoginController {// Session 中存储手机短信验证码的属性名public static final String MOBILE_SESSION_KEY = "MOBILE_SESSION_KEY";@GetMapping("/mobile/page")public String mobileLoginPage() {  // 跳转到手机短信验证码登录页面return "login-mobile";}@GetMapping("/code/mobile")@ResponseBodypublic Object sendMoblieCode(HttpServletRequest request) { // 随机生成一个 4 位的验证码String code = RandomStringUtils.randomNumeric(4);// 将手机验证码文本存储在 Session 中,设置过期时间为 10 * 60sCheckCode mobileCode = new CheckCode(code, 10 * 60);request.getSession().setAttribute(MOBILE_SESSION_KEY, mobileCode);return mobileCode;}    
}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>登录页面</title><script src="https://s3.pstatp.com/cdn/expire-1-M/jquery/3.3.1/jquery.min.js"></script>
</head>
<body><form method="post" th:action="@{/mobile/form}"><input id="mobile" name="mobile" type="text" placeholder="手机号码"><br><div><input name="mobileCode" type="text" placeholder="验证码"><button type="button" id="sendCode">获取验证码</button></div><div th:if="${param.error}"><span th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message}" style="color:red">用户名或密码错误</span></div><button type="submit">登录</button></form><script>// 获取手机短信验证码$("#sendCode").click(function () {var mobile = $('#mobile').val().trim();if(mobile == '') {alert("手机号不能为空");return;}// /code/mobile?mobile=123123123var url = "/code/mobile?mobile=" + mobile;$.get(url, function(data){alert(data);});});</script>
</body>
</html>

自定义短信验证码校验过滤器

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

  1. 带图形验证码的用户名、密码方式登录方式出现认证异常,重定向到/login/page?error
  2. 手机短信验证码方式登录出现认证异常,重定向到/mobile/page?error
/**
* 继承 SimpleUrlAuthenticationFailureHandler 处理器,该类是 failureUrl() 方法使用的认证失败处理器
*/
@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {String xRequestedWith = request.getHeader("x-requested-with");// 判断前端的请求是否为 ajax 请求if ("XMLHttpRequest".equals(xRequestedWith)) {// 认证成功,响应 JSON 数据response.setContentType("application/json;charset=utf-8");response.getWriter().write("认证失败");}else {// 用户名、密码方式登录出现认证异常,需要重定向到 /login/page?error// 手机短信验证码方式登录出现认证异常,需要重定向到 /mobile/page?error// 使用 Referer 获取当前登录表单提交请求是从哪个登录页面(/login/page 或 /mobile/page)链接过来的String refer = request.getHeader("Referer");String lastUrl = StringUtils.substringBefore(refer, "?");// 设置默认的重定向路径super.setDefaultFailureUrl(lastUrl + "?error");// 调用父类的 onAuthenticationFailure() 方法super.onAuthenticationFailure(request, response, e);}}
}
/*** 手机短信验证码校验*/
@Component
public class MobileCodeValidateFilter extends OncePerRequestFilter {private String codeParamter = "mobileCode";  // 前端输入的手机短信验证码参数名@Autowiredprivate CustomAuthenticationFailureHandler authenticationFailureHandler; // 自定义认证失败处理器@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {// 非 POST 方式的手机短信验证码提交请求不进行校验if("/mobile/form".equals(request.getRequestURI()) && "POST".equals(request.getMethod())) {try {// 检验手机验证码的合法性validate(request);} catch (ValidateCodeException e) {// 将异常交给自定义失败处理器进行处理authenticationFailureHandler.onAuthenticationFailure(request, response, e);return;}}// 放行,进入下一个过滤器filterChain.doFilter(request, response);}/*** 检验用户输入的手机验证码的合法性*/private void validate(HttpServletRequest request) {// 获取用户传入的手机验证码值String requestCode = request.getParameter(this.codeParamter);if(requestCode == null) {requestCode = "";}requestCode = requestCode.trim();// 获取 SessionHttpSession session = request.getSession();// 获取 Session 中存储的手机短信验证码CheckCode savedCode = (CheckCode) session.getAttribute(LoginController.MOBILE_SESSION_KEY);if (savedCode != null) {// 随手清除验证码,无论是失败,还是成功。客户端应在登录失败时刷新验证码session.removeAttribute(LoginController.MOBILE_SESSION_KEY);}// 校验出错,抛出异常if (StringUtils.isBlank(requestCode)) {throw new ValidateCodeException("验证码的值不能为空");}if (savedCode == null) {throw new ValidateCodeException("验证码不存在");}if (savedCode.isExpried()) {throw new ValidateCodeException("验证码过期");}if (!requestCode.equalsIgnoreCase(savedCode.getCode())) {throw new ValidateCodeException("验证码输入错误");}}
}

自定义短信验证码认证过滤器

  1. 仿照 UsernamePasswordAuthenticationToken 类进行编写
  2. 仿照 UsernamePasswordAuthenticationFilter 过滤器进行编写
public class MobileAuthenticationToken extends AbstractAuthenticationToken {private static final long serialVersionUID = 520L;private final Object principal;/*** 认证前,使用该构造器进行封装信息*/public MobileAuthenticationToken(Object principal) {super(null);     // 用户权限为 nullthis.principal = principal;   // 前端传入的手机号this.setAuthenticated(false); // 标记为未认证}/*** 认证成功后,使用该构造器封装用户信息*/public MobileAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {super(authorities);          // 用户权限集合this.principal = principal;  // 封装认证用户信息的 UserDetails 对象,不再是手机号super.setAuthenticated(true); // 标记认证成功}@Overridepublic Object getCredentials() {// 由于使用手机短信验证码登录不需要密码,所以直接返回 nullreturn null;}@Overridepublic Object getPrincipal() {return this.principal;}@Overridepublic 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);}}@Overridepublic void eraseCredentials() {// 手机短信验证码认证方式不必去除额外的敏感信息,所以直接调用父类方法super.eraseCredentials();}
}
/*** 手机短信验证码认证过滤器,仿照 UsernamePasswordAuthenticationFilter 过滤器编写*/
public class MobileAuthenticationFilter extends AbstractAuthenticationProcessingFilter {private String mobileParamter = "mobile";  // 默认手机号参数名为 mobileprivate boolean postOnly = true;    // 默认请求方式只能为 POSTprotected MobileAuthenticationFilter() {// 默认登录表单提交路径为 /mobile/form,POST 方式请求super(new AntPathRequestMatcher("/mobile/form", "POST"));}@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {//(1) 默认情况下,如果请求方式不是 POST,会抛出异常if(postOnly && !request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}else {//(2) 获取请求携带的 mobileString mobile = request.getParameter(mobileParamter);if(mobile == null) {mobile = "";}mobile = mobile.trim();//(3) 使用前端传入的 mobile 构造 Authentication 对象,标记该对象未认证// MobileAuthenticationToken 是我们自定义的 Authentication 类,后续介绍MobileAuthenticationToken authRequest = new MobileAuthenticationToken(mobile);//(4) 将请求中的一些属性信息设置到 Authentication 对象中,如:remoteAddress,sessionIdthis.setDetails(request, authRequest);//(5) 调用 ProviderManager 类的 authenticate() 方法进行身份认证return this.getAuthenticationManager().authenticate(authRequest);}}@Nullableprotected String obtainMobile(HttpServletRequest request) {return request.getParameter(this.mobileParamter);}protected void setDetails(HttpServletRequest request, MobileAuthenticationToken authRequest) {authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));}public void setMobileParameter(String mobileParamter) {Assert.hasText(mobileParamter, "Mobile par ameter must not be empty or null");this.mobileParamter = mobileParamter;}public void setPostOnly(boolean postOnly) {this.postOnly = postOnly;}public String getMobileParameter() {return mobileParamter;}
}

自定义短信验证码认证处理器

  1. 仿照 DaoAuthenticationProvider 处理器进行编写
  2. MobileAuthenticationProvider 处理器传入的 UserDetailsService 对象的类型需要我们自定义
public class MobileAuthenticationProvider implements AuthenticationProvider {private UserDetailsService userDetailsService;protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();private UserDetailsChecker authenticationChecks = new MobileAuthenticationProvider.DefaultAuthenticationChecks();/*** 处理认证*/@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {//(1) 如果入参的 Authentication 类型不是 MobileAuthenticationToken,抛出异常Assert.isInstanceOf(MobileAuthenticationToken.class, authentication, () -> {return this.messages.getMessage("MobileAuthenticationProvider.onlySupports", "Only MobileAuthenticationToken is supported");});// 获取手机号String mobile = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();//(2) 根据手机号从数据库中查询用户信息UserDetails user = this.userDetailsService.loadUserByUsername(mobile);if (user == null) {//(3) 未查询到用户信息,抛出异常throw new AuthenticationServiceException("该手机号未注册");}//(4) 检查账号是否锁定、账号是否可用、账号是否过期、密码是否过期this.authenticationChecks.check(user);//(5) 查询到了用户信息,则认证通过,构建标记认证成功用户信息类对象 AuthenticationTokenMobileAuthenticationToken result = new MobileAuthenticationToken(user, user.getAuthorities());// 需要把认证前 Authentication 对象中的 details 信息加入认证后的 Authenticationresult.setDetails(authentication.getDetails());return result;}/*** ProviderManager 管理器通过此方法来判断是否采用此 AuthenticationProvider 类* 来处理由 AuthenticationFilter 过滤器传入的 Authentication 对象*/@Overridepublic boolean supports(Class<?> authentication) {// isAssignableFrom 返回 true 当且仅当调用者为父类.class,参数为本身或者其子类.class// ProviderManager 会获取 MobileAuthenticationFilter 过滤器传入的 Authentication 类型// 所以当且仅当 authentication 的类型为 MobileAuthenticationToken 才返回 truereturn MobileAuthenticationToken.class.isAssignableFrom(authentication);}/*** 此处传入自定义的 MobileUserDetailsSevice 对象*/public void setUserDetailsService(UserDetailsService userDetailsService) {this.userDetailsService = userDetailsService;}public UserDetailsService getUserDetailsService() {return userDetailsService;}/*** 检查账号是否锁定、账号是否可用、账号是否过期、密码是否过期*/private class DefaultAuthenticationChecks implements UserDetailsChecker {private DefaultAuthenticationChecks() {}@Overridepublic void check(UserDetails user) {if (!user.isAccountNonLocked()) {throw new LockedException(MobileAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"));} else if (!user.isEnabled()) {throw new DisabledException(MobileAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"));} else if (!user.isAccountNonExpired()) {throw new AccountExpiredException(MobileAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"));} else if (!user.isCredentialsNonExpired()) {throw new CredentialsExpiredException(MobileAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.credentialsExpired", "User credentials have expired"));}}}
}
@Service
public class MobileUserDetailsService implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String mobile) throws UsernameNotFoundException {//(1) 从数据库尝试读取该用户User user = userMapper.selectByMobile(mobile);// 用户不存在,抛出异常if (user == null) {throw new UsernameNotFoundException("用户不存在");}//(2) 将数据库形式的 roles 解析为 UserDetails 的权限集合// AuthorityUtils.commaSeparatedStringToAuthorityList() 是 Spring Security 提供的方法,用于将逗号隔开的权限集字符串切割为可用权限对象列表user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));//(3) 返回 UserDetails 对象return user;}
}

自定义短信验证码认证方式配置类

  1. 将上述组件进行管理,仿照 SecurityConfigurerAdapter类进行编写
  2. 绑定到最终的安全配置类 SpringSecurityConfig 中
@Component
public class MobileAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {@Autowiredprivate CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;  // 自定义认证成功处理器@Autowiredprivate CustomAuthenticationFailureHandler customAuthenticationFailureHandler;  // 自定义认证失败处理器@Autowiredprivate MobileCodeValidateFilter mobileCodeValidaterFilter;  // 手机短信验证码校验过滤器@Autowiredprivate MobileUserDetailsService userDetailsService;  // 手机短信验证方式的 UserDetail@Overridepublic void configure(HttpSecurity http) throws Exception {//(1) 将短信验证码认证的自定义过滤器绑定到 HttpSecurity 中//(1.1) 创建手机短信验证码认证过滤器的实例 filerMobileAuthenticationFilter filter = new MobileAuthenticationFilter();//(1.2) 设置 filter 使用 AuthenticationManager(ProviderManager 接口实现类) 认证管理器// 多种登录方式应该使用同一个认证管理器实例,所以获取 Spring 容器中已经存在的 AuthenticationManager 实例AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);filter.setAuthenticationManager(authenticationManager);//(1.3) 设置 filter 使用自定义成功和失败处理器filter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler);filter.setAuthenticationFailureHandler(customAuthenticationFailureHandler);//(1.4) 设置 filter 使用 SessionAuthenticationStrategy 会话管理器// 多种登录方式应该使用同一个会话管理器实例,获取 Spring 容器已经存在的 SessionAuthenticationStrategy 实例SessionAuthenticationStrategy sessionAuthenticationStrategy = http.getSharedObject(SessionAuthenticationStrategy.class);filter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy);//(1.5) 在 UsernamePasswordAuthenticationFilter 过滤器之前添加 MobileCodeValidateFilter 过滤器// 在 UsernamePasswordAuthenticationFilter 过滤器之后添加 MobileAuthenticationFilter 过滤器http.addFilterBefore(mobileCodeValidaterFilter, UsernamePasswordAuthenticationFilter.class);http.addFilterAfter(filter, UsernamePasswordAuthenticationFilter.class);//(2) 将自定义的 MobileAuthenticationProvider 处理器绑定到 HttpSecurity 中//(2.1) 创建手机短信验证码认证过滤器的 AuthenticationProvider 实例,并指定所使用的 UserDetailsServiceMobileAuthenticationProvider provider = new MobileAuthenticationProvider();provider.setUserDetailsService(userDetailsService);//(2.2) 将该 AuthenticationProvider 实例绑定到 HttpSecurity 中http.authenticationProvider(provider);}
}
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate UserDetailServiceImpl userDetailsService;@Autowiredprivate CustomAuthenticationSuccessHandler authenticationSuccessHandler; // 自定义认证成功处理器@Autowiredprivate CustomAuthenticationFailureHandler authenticationFailureHandler; // 自定义认证失败处理器@Autowiredprivate ImageCodeValidateFilter imageCodeValidateFilter; // 自定义过滤器(图形验证码校验)@Autowiredprivate MobileAuthenticationConfig mobileAuthenticationConfig; // 手机短信验证码认证方式的配置类/*** 密码编码器,密码不能明文存储*/@Beanpublic BCryptPasswordEncoder passwordEncoder() {// 使用 BCryptPasswordEncoder 密码编码器,该编码器会将随机产生的 salt 混入最终生成的密文中return new BCryptPasswordEncoder();}/*** 定制用户认证管理器来实现用户认证*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {       // 不再使用内存方式存储用户认证信息,而是动态从数据库中获取auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());}/*** 定制基于 HTTP 请求的用户访问控制*/@Overrideprotected void configure(HttpSecurity http) throws Exception {// 启动 form 表单登录http.formLogin()// 设置登录页面的访问路径,默认为 /login,GET 请求;该路径不设限访问.loginPage("/login/page")// 设置登录表单提交路径,默认为 loginPage() 设置的路径,POST 请求.loginProcessingUrl("/login/form")// 使用自定义的认证成功和失败处理器.successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler);// 开启基于 HTTP 请求访问控制http.authorizeRequests()// 以下访问不需要任何权限,任何人都可以访问.antMatchers("/login/page", "/code/image","/mobile/page", "/code/mobile").permitAll()// 其它任何请求访问都需要先通过认证.anyRequest().authenticated();// 关闭 csrf 防护http.csrf().disable();// 将自定义过滤器(图形验证码校验)添加到 UsernamePasswordAuthenticationFilter 之前http.addFilterBefore(imageCodeValidateFilter, UsernamePasswordAuthenticationFilter.class);// 将手机短信验证码认证的配置与当前的配置绑定http.apply(mobileAuthenticationConfig);}/*** 定制一些全局性的安全配置,例如:不拦截静态资源的访问*/@Overridepublic void configure(WebSecurity web) throws Exception {// 静态资源的访问不需要拦截,直接放行web.ignoring().antMatchers("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/898579.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

NineData社区版抢先体验,获取无人机、双肩包、充电宝等周边福利

NineData社区版现已正式上线,支持通过 Docker 部署至本地,保障数据安全与操作留存在本地。为庆祝发布,NineData 开启技术体验官征文活动,邀请开发者分享安装、使用及优化建议等经验。活动设置丰厚奖品,包括大疆无人机、高级旅行箱等,并提供多种加分机制,如真实场景分享和…

EtherCAT转Profinet揭秘网关模块促成西门子PLC与伺服电机通讯的协议转换秘诀案例​

一. 案例背景 西门子1200PLC通过捷米特JM-ECTM-PN(EtherCAT转ProfiNet)网关将松下伺服电机(包括不限于型号MHMFO22D1U2M)或EtherCAT协议的其它设备或连接到ProfiNetPLC上,并在正常运行中支持EtherCAT协议。本产品可作为EtherCAT主站,做为西门子S7-1200系列PLC的从站并在监…

vcftools根据个体id提取数据和删除数据

001、vcftools根据个体id提取数据[b20223040323@admin2 test5]$ ls id.list outcome.vcf [b20223040323@admin2 test5]$ cat id.list GMM5 GMM6 GMM7 GMM8 [b20223040323@admin2 test5]$ grep "^#" outcome.vcf | tail -n 1 | cut -f 10- GMM1 GMM2 GMM3 G…

EtherCAT转Profinet网关助力协议转换推动西门子PLC与伺服电机通讯进程案例​

一、项目背景 在自动化生产系统中,经常会遇到不同品牌设备之间需要进行数据交互和协同工作的情况。本案例中,需要实现西门子1200PLC与松下A6B系列伺服驱动器的通讯,以实现对伺服电机的精确控制。由于两者采用不同的通讯协议,直接通讯存在困难,因此引入JM-ECTM-PN协议转换网…

No.64 Vue---vue引入第三方

一、Swiper官网: https://www.swiper.com.cn/ https://swiperjs.com/vue 安装swiper: 创建一个组件:MySwiper.vue<template><div class="hello"></div><swiper><swiper-slide><img src="D:\JS_proj\ES6Module116\VueDemo\vu…

魔方求解器程序(层先法,java版本)

实现了一个三阶魔方的层先法求解程序:https://github.com/davelet/java-puzzle-resolver 欢迎试用。用法 1. 随机试用 不关注起始状态的话可以用程序的随机拧乱工具打乱然后复原:private Cube cube;private CubeSolver cubeSolver;private CubeShuffler cubeShuffler;@Before…

8款热门CRM系统盘点!优缺点分析,帮你选对适合的!

现在做生意,客户就是金饭碗,谁能把客户维护好,谁就能在市场上占一席之地。 可是,客户多了,信息杂了,跟进不到位、管理混乱、流失率高……这些问题有没有让你头大?所以,一款好用的CRM(客户管理系统)真的太重要了!小编已经整理好的CRM系统模板,自取>>https://s…

微信内H5页面点击链接打开微信小程序

由于公司产品需求,需要在H5页面内打开小程序,查了微信文档解决了问题,解决如下: 1.打开小程序公众平台>账号设置>隐私与安全>配置明文scheme拉起此小程序 配置好后,在H5页面跳转到小程序 window.location.href = weixin://dl/business/?appid=*APPID*&path=…

word中的endnote文献引用字体颜色更改为蓝色,且无下划线

1.Endnote设置: 1.1在word插件Endnote X9,找到下图的位置, 1.2勾选下面的两个选项,2.word设置 2.1在word中,找到“开始”---“样式”---“超链接”,鼠标右键“修改”,取消下划线。

未来十年之内最好的创业的时间点

未来十年之内最好的创业的时间点,超级个体未来十年之内最好的创业的时间点 ‍今天这条视频非常重要,凡是想创业的,或者说你35岁左右,你希望你的下半辈子能够有一次财富升为的话呢,一定要认真听,呃,我先讲结论啊,今年是一个未来十年之内最好的创业的时间点,如果你今年不…

优化GreatSQL日志文件空间占用

优化GreatSQL日志文件空间占用 GreatSQL对于日志文件磁盘空间占用,做了一些优化,对于binlog、relay log、slow log和audit log的总空间占用进行了限制,使DBA免除了大量日志生成导致磁盘满的顾虑,极大的方便了数据库磁盘空间管理。 1.binlog二进制日志binlog_space_limitGre…

day:21 python——列表数据处理

一.列表的介绍和定义 1 .列表 类型: <class list> 2.符号:[] 3.定义列表: 方式1:[] 通过[] 来定义 list=[1,2,3,4,6] print(type(list)) #<class list>方式2: 通过list 转换 str2="12345" print(type(str2)) #<class str> list2=list(str2) prin…