之前我们讲了短信验证码开发,现在需要使用短信登录;具体流程如下:
之前密码登录我们详细介绍过其详细流程如下:
短信登录的话,首先我们需要明确,短信登录不能在此流程里面出现和改动,因为这是两种不同的登录方式,不能把两种不同的登录方式混合在一套代码里面去维护,这样代码质量会大大降低。维护起来也很困难。 我们需要仿造密码登录流程自己写一套流程。 我们需要写4个东西:
注意:
因为短信登录功能无论在浏览器端还是手机端都会用,我们我们把他放到spring security core里面
我们拷贝出:UsernamePasswordAuthenticationToken代码,然后在此基础上修改:
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 420L;
private final Object principal;
/**
* 没登录之前,principal我们使用手机号
* @param mobile
*/
public SmsCodeAuthenticationToken(String mobile) {
super((Collection)null);
this.principal = mobile;
this.setAuthenticated(false);
}
/**
* 登录认证之后,principal我们使用用户信息
* @param principal
* @param authorities
*/
public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true);
}
public Object getCredentials() {
return null;
}
public Object getPrincipal() {
return this.principal;
}
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");
} else {
super.setAuthenticated(false);
}
}
public void eraseCredentials() {
super.eraseCredentials();
}
}
我们拷贝出:UsernamePasswordAuthenticationFilter代码,然后在此基础上修改:
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
/**
* 在我们认证过程中是不需要密码,认证信息是手机号
*/
public static final String YXM_FORM_MOBILE_KEY = "mobile";
/**
* 请求中携带参数的名字是什么?
*/
private String mobileParameter = "mobile";
/**
* 此过滤器只处理post请求
*/
private boolean postOnly = true;
public SmsCodeAuthenticationFilter() {
//指明当前过滤器处理的请求是什么?/authentication/mobile --这个地址不能写错,不然不会走此认证过滤器方法
super(new AntPathRequestMatcher("/authentication/mobile", "POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String mobile = this.obtainMobile(request);
if (mobile == null) {
mobile = "";
}
mobile = mobile.trim();
//实例化未认证的token
SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
this.setDetails(request, authRequest);
/**
* AuthenticationManager进行调用
*/
return this.getAuthenticationManager().authenticate(authRequest);
}
}
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(this.mobileParameter);
}
protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
public void setMobileParameter(String mobileParameter) {
Assert.hasText(mobileParameter, "Username parameter must not be empty or null");
this.mobileParameter = mobileParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getMobileParameter() {
return this.mobileParameter;
}
}
SmsCodeAuthenticationProvider 开发实现AuthenticationProvider接口
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
/**
* 校验逻辑很简单:用SmsCodeAuthenticationToken信息调用UserDetailService去数据库校验
* 将此对象变成一个已经认证的认证数据
*/
SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());
if(user == null){
throw new InternalAuthenticationServiceException("无法获取用户信息");
}
//.没有错误说明认证成功
final SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
/**
* 此方法就是检验AuthenticationManager会使用哪个provider
* @param authentication
* @return
*/
@Override
public boolean supports(Class<?> authentication) {
//此方法就是检验AuthenticationManager会使用哪个provider
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
}
我们需要短信验证码拦截器:在请求进入SmsCodeAuthenticationFilter之前;短信验证码的验证逻辑和图片验证码的验证逻辑一样。所以我们直接在上面修改:SmsCodeFilter 代码基本是一样的,只是我们拦截的请求变化了。
public class SmsCodeFilter extends OncePerRequestFilter implements InitializingBean {
private AuthenticationFailureHandler authenticationFailureHandler;
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
private Logger logger = LoggerFactory.getLogger(getClass());
//需要校验的url都在这里面添加
private Set<String> urls = new HashSet<>();
private AntPathMatcher antPathMatcher = new AntPathMatcher();
//此处不用注解@Autowire 而是使用setter方法将在WebSecurityConfig设置
private SecurityProperties securityProperties;
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
/**
* 拦截短信验证码需要拦截的地址url(包括自定义url和登录时候url):这些url都需要经过这个过滤器
*/
String[] configUrls = StringUtils.splitByWholeSeparatorPreserveAllTokens(securityProperties.getCode().getSms().getUrl(), ",");
if(configUrls!=null && configUrls.length>0){
for (String configUrl:configUrls) {
urls.add(configUrl);
}
}
//"/authentication/moble 一定会校验验证码的
urls.add("/authentication/mobile");
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
logger.info("验证码过滤器:doFilterInternal: requestURI:[{}] requestMethod:[{}]",request.getRequestURI(),request.getMethod());
/**
* 如果是需要认证请求,我们进行家宴
* 如果校验失败,使用我们自定义的校验失败处理类处理
* 如果不需要认证,我们放行进入下一个Filter
*/
//在afterPropertiesSet执行之后,url初始化完毕之后,但是此时我们判断不能用StringUtils.equals,我们我们urls里面有 url: /user,/user/* 带星号的配置
// 用户请求有可能是/user/1、/user/2 我们需要使用Spring的 AntPathMatcher
boolean action = false;
for (String url:urls) {
//如果配置的url和请求的url相同时候,需要校验
if(antPathMatcher.match(url,request.getRequestURI())){
action = true;
}
}
if(action){
try{
validate(new ServletWebRequest(request));
}catch (ValidateCodeException e){
authenticationFailureHandler.onAuthenticationFailure(request,response,e);
//抛出异常校验失败,不再走小面过滤器执行链
return;
}
}
filterChain.doFilter(request,response);
}
private void validate(ServletWebRequest servletWebRequest) throws ServletRequestBindingException {
//1.获取存放到session中的验证码
ValidateCode codeInSession = (ValidateCode)sessionStrategy.getAttribute(servletWebRequest, ValidateCodeProcessor.SESSION_KEY_PREFIX+"SMS");
//2.获取请求中的验证码
String codeInRequest = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "smsCode");
if(StringUtils.isBlank(codeInRequest)){
throw new ValidateCodeException("验证码的值不能为空");
}
if(codeInSession == null){
throw new ValidateCodeException("验证码不存在");
}
if(codeInSession.isExpried()){
sessionStrategy.removeAttribute(servletWebRequest,ValidateCodeProcessor.SESSION_KEY_PREFIX+"SMS");
throw new ValidateCodeException("验证码已过期");
}
if(!StringUtils.equals(codeInSession.getCode(),codeInRequest)){
throw new ValidateCodeException("验证码不匹配");
}
sessionStrategy.removeAttribute(servletWebRequest,ValidateCodeProcessor.SESSION_KEY_PREFIX+"SMS");
}
public AuthenticationFailureHandler getAuthenticationFailureHandler() {
return authenticationFailureHandler;
}
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.authenticationFailureHandler = authenticationFailureHandler;
}
public SecurityProperties getSecurityProperties() {
return securityProperties;
}
public void setSecurityProperties(SecurityProperties securityProperties) {
this.securityProperties = securityProperties;
}
}