实现短信验证码登录
Posted 木兮同学
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实现短信验证码登录相关的知识,希望对你有一定的参考价值。
文章目录
之前文章都是基于用户名密码登录,第六章图形验证码登录其实还是用户名密码登录,只不过多了一层图形验证码校验而已;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">发送验证码</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 PersistentTokenRep以上是关于实现短信验证码登录的主要内容,如果未能解决你的问题,请参考以下文章
RedisRedis 的共享 session 应用(短信登录)
RedisRedis 的共享 session 应用(短信登录)