之前我们讲了短信验证码开发,现在需要使用短信登录;具体流程如下:
之前密码登录我们详细介绍过其详细流程如下:
短信登录的话,首先我们需要明确,短信登录不能在此流程里面出现和改动,因为这是两种不同的登录方式,不能把两种不同的登录方式混合在一套代码里面去维护,这样代码质量会大大降低。维护起来也很困难。 我们需要仿造密码登录流程自己写一套流程。 我们需要写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; } }