Spring Security实现短信验证码登录功能的方法
这篇文章主要讲解了Spring Security实现短信验证码登录功能的方法,内容清晰明了,对此有兴趣的小伙伴可以学习一下,相信大家阅读完之后会有帮助。
之前文章都是基于用户名密码登录,第六章图形验证码登录其实还是用户名密码登录,只不过多了一层图形验证码校验而已;Spring Security默认提供的认证流程就是用户名密码登录,整个流程都已经固定了,虽然提供了一些接口扩展,但是有些时候我们就需要有自己特殊的身份认证逻辑,比如用短信验证码登录,它和用户名密码登录的逻辑是不一样的,这时候就需要重新写一套身份认证逻辑。
开发短信验证码接口
获取验证码
短信验证码的发送获取逻辑和图片验证码类似,这里直接贴出代码。
@GetMapping("/code/sms") public void createSmsCode(HttpServletRequest request, HttpServletResponse response) throws Exception { // 创建验证码 ValidateCode smsCode = createCodeSmsCode(request); // 将验证码放到session中 sessionStrategy.setAttribute(new ServletWebRequest(request), SMS_CODE_SESSION_KEY, smsCode); String mobile = ServletRequestUtils.getRequiredStringParameter(request, "mobile"); // 发送验证码 smsCodeSender.send(mobile, smsCode.getCode()); }
前端代码
<tr> <td>手机号:</td> <td><input type="text" name="mobile" value="13012345678"></td> </tr> <tr> <td>短信验证码:</td> <td> <input type="text" name="smsCode"> <a href="/code/sms?mobile=13012345678" rel="external nofollow" >发送验证码</a> </td> </tr>
短信验证码流程原理
短信验证码登录和用户名密码登录对比
步骤流程
- 首先点击登录应该会被
SmsAuthenticationFilter
过滤器处理,这个过滤器拿到请求以后会在登录请求中拿到手机号,然后封装成自定义的一个SmsAuthenticationToken(未认证)。 - 这个Token也会传给AuthenticationManager,因为
AuthenticationManager
整个系统只有一个,它会检索系统中所有的AuthenticationProvider,这时候我们要提供自己的SmsAuthenticationProvider
,用它来校验自己写的SmsAuthenticationToken的手机号信息。 - 在校验的过程中同样会调用
UserDetailsService
,把手机号传给它让它去读用户信息,去判断是否能登录,登录成功的话再把SmsAuthenticationToken标记为已认证。 - 到这里为止就是短信验证码的认证流程,上面的流程并没有提到校验验证码信息,其实它的验证流程和图形验证码验证流程也是类似,同样是
在SmsAuthenticationFilter过滤器之前加一个过滤器来验证短信验证码
。
代码实现
SmsCodeAuthenticationToken
- 作用:封装认证Token
- 实现:可以继承AbstractAuthenticationToken抽象类,该类实现了Authentication接口
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; private final Object principal; /** * 进入SmsAuthenticationFilter时,构建一个未认证的Token * * @param mobile */ public SmsCodeAuthenticationToken(String mobile) { super(null); this.principal = mobile; setAuthenticated(false); } /** * 认证成功以后构建为已认证的Token * * @param principal * @param authorities */ public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; super.setAuthenticated(true); } @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"); } super.setAuthenticated(false); } @Override public void eraseCredentials() { super.eraseCredentials(); } }
SmsCodeAuthenticationFilter
- 作用:处理短信登录的请求,构建Token,把请求信息设置到Token中。
- 实现:该类可以模仿UsernamePasswordAuthenticationFilter类,继承AbstractAuthenticationProcessingFilter抽象类
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter { private String mobileParameter = "mobile"; private boolean postOnly = true; /** * 表示要处理的请求路径 */ public SmsCodeAuthenticationFilter() { super(new AntPathRequestMatcher("/authentication/mobile", "POST")); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } String mobile = obtainMobile(request); if (mobile == null) { mobile = ""; } mobile = mobile.trim(); SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile); // 把请求信息设到Token中 setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } /** * 获取手机号 */ protected String obtainMobile(HttpServletRequest request) { return request.getParameter(mobileParameter); } protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) { authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); } public void setMobileParameter(String usernameParameter) { Assert.hasText(usernameParameter, "Username parameter must not be empty or null"); this.mobileParameter = usernameParameter; } public void setPostOnly(boolean postOnly) { this.postOnly = postOnly; } public final String getMobileParameter() { return mobileParameter; } }
SmsAuthenticationProvider
- 作用:提供认证Token的校验逻辑,配置为能够支持SmsCodeAuthenticationToken的校验
- 实现:实现AuthenticationProvider接口,实现其两个方法。
public class SmsCodeAuthenticationProvider implements AuthenticationProvider { private UserDetailsService userDetailsService; /** * 进行身份认证的逻辑 * * @param authentication * @return * @throws AuthenticationException */ @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication; UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal()); if (user == null) { throw new InternalAuthenticationServiceException("无法获取用户信息"); } SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities()); authenticationResult.setDetails(authenticationToken.getDetails()); return authenticationResult; } /** * 表示支持校验的Token,这里是SmsCodeAuthenticationToken * * @param authentication * @return */ @Override public boolean supports(Class<?> authentication) { return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication); } public UserDetailsService getUserDetailsService() { return userDetailsService; } public void setUserDetailsService(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } }
ValidateCodeFilter
- 作用:校验短信验证码
- 实现:和图形验证码类似,继承OncePerRequestFilter接口防止多次调用,主要就是验证码验证逻辑,验证通过则继续下一个过滤器。
@Component("validateCodeFilter") public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean { /** * 验证码校验失败处理器 */ @Autowired private AuthenticationFailureHandler authenticationFailureHandler; /** * 系统配置信息 */ @Autowired private SecurityProperties securityProperties; /** * 系统中的校验码处理器 */ @Autowired private ValidateCodeProcessorHolder validateCodeProcessorHolder; /** * 存放所有需要校验验证码的url */ private Map<String, ValidateCodeType> urlMap = new HashMap<>(); /** * 验证请求url与配置的url是否匹配的工具类 */ private AntPathMatcher pathMatcher = new AntPathMatcher(); /** * 初始化要拦截的url配置信息 */ @Override public void afterPropertiesSet() throws ServletException { super.afterPropertiesSet(); urlMap.put("/authentication/mobile", ValidateCodeType.SMS); addUrlToMap(securityProperties.getCode().getSms().getUrl(), ValidateCodeType.SMS); } /** * 讲系统中配置的需要校验验证码的URL根据校验的类型放入map * * @param urlString * @param type */ protected void addUrlToMap(String urlString, ValidateCodeType type) { if (StringUtils.isNotBlank(urlString)) { String[] urls = StringUtils.splitByWholeSeparatorPreserveAllTokens(urlString, ","); for (String url : urls) { urlMap.put(url, type); } } } /** * 验证短信验证码 * * @param request * @param response * @param chain * @throws ServletException * @throws IOException */ @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { ValidateCodeType type = getValidateCodeType(request); if (type != null) { logger.info("校验请求(" + request.getRequestURI() + ")中的验证码,验证码类型" + type); try { // 进行验证码的校验 validateCodeProcessorHolder.findValidateCodeProcessor(type) .validate(new ServletWebRequest(request, response)); logger.info("验证码校验通过"); } catch (ValidateCodeException exception) { // 如果校验抛出异常,则交给我们之前文章定义的异常处理器进行处理 authenticationFailureHandler.onAuthenticationFailure(request, response, exception); return; } } // 继续调用后边的过滤器 chain.doFilter(request, response); } /** * 获取校验码的类型,如果当前请求不需要校验,则返回null * * @param request * @return */ private ValidateCodeType getValidateCodeType(HttpServletRequest request) { ValidateCodeType result = null; if (!StringUtils.equalsIgnoreCase(request.getMethod(), "GET")) { Set<String> urls = urlMap.keySet(); for (String url : urls) { if (pathMatcher.match(url, request.getRequestURI())) { result = urlMap.get(url); } } } return result; } }
添加配置
SmsCodeAuthenticationSecurityConfig
作用:配置SmsCodeAuthenticationFilter,后面需要把这些配置加到主配置类BrowserSecurityConfig
@Component public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { @Autowired private AuthenticationSuccessHandler meicloudAuthenticationSuccessHandler; @Autowired private AuthenticationFailureHandler meicloudAuthenticationFailureHandler; @Autowired private UserDetailsService userDetailsService; @Autowired private PersistentTokenRepository persistentTokenRepository; @Override public void configure(HttpSecurity http) throws Exception { SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter(); // 设置AuthenticationManager smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); // 设置登录成功处理器 smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(meicloudAuthenticationSuccessHandler); // 设置登录失败处理器 smsCodeAuthenticationFilter.setAuthenticationFailureHandler(meicloudAuthenticationFailureHandler); String key = UUID.randomUUID().toString(); smsCodeAuthenticationFilter.setRememberMeServices(new PersistentTokenBasedRememberMeServices(key, userDetailsService, persistentTokenRepository)); SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider(); smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService); // 将自己写的Provider加到Provider集合里去 http.authenticationProvider(smsCodeAuthenticationProvider) .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } }
BrowserSecurityConfig
作用:主配置类;添加短信验证码配置类、添加SmsCodeAuthenticationSecurityConfig配置
@Configuration public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Autowired private SecurityProperties securityProperties; @Autowired private DataSource dataSource; @Autowired private UserDetailsService userDetailsService; @Autowired private AuthenticationSuccessHandler meicloudAuthenticationSuccessHandler; @Autowired private AuthenticationFailureHandler meicloudAuthenticationFailureHandler; @Autowired private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig; @Override protected void configure(HttpSecurity http) throws Exception { // 验证码校验过滤器 ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter(); // 将验证码校验过滤器加到 UsernamePasswordAuthenticationFilter 过滤器之前 http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) .formLogin() // 当用户登录认证时默认跳转的页面 .loginPage("/authentication/require") // 以下这行 UsernamePasswordAuthenticationFilter 会知道要处理表单的 /authentication/form 请求,而不是默认的 /login .loginProcessingUrl("/authentication/form") .successHandler(meicloudAuthenticationSuccessHandler) .failureHandler(meicloudAuthenticationFailureHandler) // 配置记住我功能 .and() .rememberMe() // 配置TokenRepository .tokenRepository(persistentTokenRepository()) // 配置Token过期时间 .tokenValiditySeconds(3600) // 最终拿到用户名之后,使用UserDetailsService去做登录 .userDetailsService(userDetailsService) .and() .authorizeRequests() // 排除对 "/authentication/require" 和 "/meicloud-signIn.html" 的身份验证 .antMatchers("/authentication/require", securityProperties.getBrowser().getSignInPage(), "/code/*").permitAll() // 表示所有请求都需要身份验证 .anyRequest() .authenticated() .and() .csrf().disable()// 暂时把跨站请求伪造的功能关闭掉 // 相当于把smsCodeAuthenticationSecurityConfig里的配置加到上面这些配置的后面 .apply(smsCodeAuthenticationSecurityConfig); } /** * 记住我功能的Token存取器配置 * * @return */ @Bean public PersistentTokenRepository persistentTokenRepository() { JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl(); tokenRepository.setDataSource(dataSource); // 启动的时候自动创建表,建表语句 JdbcTokenRepositoryImpl 已经都写好了 tokenRepository.setCreateTableOnStartup(true); return tokenRepository; } }
看完上述内容,是不是对Spring Security实现短信验证码登录功能的方法有进一步的了解,如果还想学习更多内容,欢迎关注行业资讯频道。