经过前面七章的学习,我们已经算入门 Spring Security 了,下面我们学习如何对 Spring Security 进行扩展,来实现短信验证码方式登录。
注意:
为了方便讲解,本篇文章代码直接在 《SpringBoot集成Spring Security(1)——入门程序》 上进行开发。
并且为了省去与本篇主题无关的代码,短信验证码只是一个模拟。如果你需要具体的实际例子,在下面的源码链接中除了包括每一章的代码外,还包括从头到尾的完整整合代码,方便大家参考学习。
源码地址:https://github.com/jitwxs/blog-sample/tree/master/springboot-security
一、理论说明
在开始编码前,先理解下短信验证码的实现流程。如果你能对《SpringBoot集成Spring Security(7)——认证流程》这篇文章有一定的了解的话,那么这篇文章的学习你会轻松许多。
1.1 用户名密码登录逻辑
废话不多说,在上一篇文章中,以标准的用户名密码登录为例,讲解了整个认证流程。大致流程如下:
- 先进入
UsernamePasswordAuthenticationFilter
中,根据输入的用户名和密码信息,构造出一个暂时没有鉴权的UsernamePasswordAuthenticationToken
,并将 UsernamePasswordAuthenticationToken 交给AuthenticationManager
处理。 AuthenticationManager
本身并不做验证处理,他通过 for-each 遍历找到符合当前登录方式的一个 AuthenticationProvider,并交给它进行验证处理,对于用户名密码登录方式,这个 Provider 就是DaoAuthenticationProvider
。- 在这个 Provider 中进行一系列的验证处理,如果验证通过,就会重新构造一个添加了鉴权的
UsernamePasswordAuthenticationToken
,并将这个 token 传回到UsernamePasswordAuthenticationFilter
中。 - 在该 Filter 的父类
AbstractAuthenticationProcessingFilter
中,会根据上一步验证的结果,跳转到 successHandler 或者是 failureHandler。

1.2 短信验证码登录逻辑
我们可以仿照用户名密码登录的逻辑,来实现短信验证码的登陆逻辑。
- 用户名密码登录有个 UsernamePasswordAuthenticationFilter ,我们搞一个
SmsAuthenticationFilter
,代码粘过来改一改。 - 用户名密码登录需要 UsernamePasswordAuthenticationToken,我们搞一个
SmsAuthenticationToken
,代码粘过来改一改。 - 用户名密码登录需要 DaoAuthenticationProvider,我们模仿它也 implenments AuthenticationProvider,叫做
SmsAuthenticationProvider
。
我们自己搞了上面三个类以后,想要实现的效果如上图所示。当我们使用短信验证码登录的时候:
- 先经过
SmsAuthenticationFilter
,构造一个没有鉴权的SmsAuthenticationToken
,然后交给 AuthenticationManager 处理。 - AuthenticationManager 通过 for-each 挑选出一个合适的 provider 进行处理,当然我们希望这个 provider 要是
SmsAuthenticationProvider
。 - 验证通过后,重新构造一个有鉴权的
SmsAuthenticationToken
,并返回给SmsAuthenticationFilter
。 - filter 根据上一步的验证结果,跳转到成功或者失败的处理逻辑。
二、代码实战
请通过文章开头 github 链接下载第一章代码,或者参看《SpringBoot集成Spring Security(1)——入门程序》初始化项目,这里就不再赘述了。
2.1 SmsAuthenticationToken
首先我们编写 SmsAuthenticationToken
,这里直接参考 UsernamePasswordAuthenticationToken
源码,直接粘过来,改一改。
步骤:
principal
原本代表用户名,这里保留,只是代表了手机号码。credentials
原本代码密码,短信登录用不到,直接删掉。SmsCodeAuthenticationToken()
两个构造方法一个是构造没有鉴权的,一个是构造有鉴权的。- 剩下的几个方法去除无用属性即可。
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import java.util.Collection;
/**
* 短信登录 AuthenticationToken,模仿 UsernamePasswordAuthenticationToken 实现
* @author jitwxs
* @since 2019/1/9 13:47
*/
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
/**
* 在 UsernamePasswordAuthenticationToken 中该字段代表登录的用户名,
* 在这里就代表登录的手机号码
*/
private final Object principal;
/**
* 构建一个没有鉴权的 SmsCodeAuthenticationToken
*/
public SmsCodeAuthenticationToken(Object principal) {
super(null);
this.principal = principal;
setAuthenticated(false);
}
/**
* 构建拥有鉴权的 SmsCodeAuthenticationToken
*/
public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
// must use super, as we override
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();
}
}
2.2 SmsAuthenticationFilter
然后编写 SmsAuthenticationFilter
,参考 UsernamePasswordAuthenticationFilter 的源码,直接粘过来,改一改。
步骤:
- 原本的静态字段有 username 和 password,都干掉,换成我们的手机号字段。
SmsCodeAuthenticationFilter()
中指定了这个 filter 的拦截 Url,我指定为 post 方式的/sms/login
。- 剩下来的方法把无效的删删改改就好了。
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.Assert;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 短信登录的鉴权过滤器,模仿 UsernamePasswordAuthenticationFilter 实现
* @author jitwxs
* @since 2019/1/9 13:52
*/
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
/**
* form表单中手机号码的字段name
*/
public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";
private<