转载

15.SpringSecurity-短信登录开发

前言

之前我们讲了短信验证码开发,现在需要使用短信登录;具体流程如下:

之前密码登录我们详细介绍过其详细流程如下:

内容

15.SpringSecurity-短信登录开发

1.1 密码登录详细流程

  1. 首先密码登录请求:/authentication/form会给到UsernamePasswordAuthenticationFilter(此过滤器会拿出用户名/密码,组装成未认证的UsernamePasswordAuthenticationToken;传给AuthenticationManager)
  2. AuthenticationManager会从一堆AuthenticationProvider里面去挑选一个provider来处理认证请求(挑选的依据是:AuthenticationProvider有一个support方法,此support判断当前的provider是否支持你传递过来的token,如果支持的话就用对应的provider处理:成未认证的UsernamePasswordAuthenticationToken;这里用到DaoAuthenticationProvider)
  3. DaoAuthenticationProviderren认证过程中会调用UserDetailsService去获取用户信息UserDetails;跟请求传过来的登录信息做一些比较;判断是否可以认证通过。
  4. 认证通过的话最终会把:未认证的UsernamePasswordAuthenticationToken作为一个标记:Authentication(已认证)

1.2 短信登录详细流程

短信登录的话,首先我们需要明确,短信登录不能在此流程里面出现和改动,因为这是两种不同的登录方式,不能把两种不同的登录方式混合在一套代码里面去维护,这样代码质量会大大降低。维护起来也很困难。 我们需要仿造密码登录流程自己写一套流程。 我们需要写4个东西:

  1. 短信登录请求(/authentication/mobile)给SmsAuthenticationFilter(此过滤器会拿出mobile,组装成未认证的SmsAuthenticationToken;传给AuthenticationManager:因为AuthenticationManager整个系统就只有一个)。
  2. AuthenticationManager会从一堆AuthenticationProvider里面去挑选一个provider来处理认证请求(挑选的依据是:AuthenticationProvider有一个support方法,此support判断当前的provider是否支持你传递过来的token,如果支持的话就用对应的provider处理:成未认证的SmsAuthenticationFilter;这里用到SmsAuthenticationProvider:需要我们自己实现)
  3. SmsAuthenticationProvider认证过程中会调用UserDetailsService去获取用户信息UserDetails;跟请求传过来的登录信息做一些比较;判断是否可以认证通过。

注意:

  1. 此过程中说了3个东西:Filter、Token、Provider;这3个类作用的过程中,我们不会去校验短信验证码这里只是根据手机号去做用户信息认证。短信验证码的校验类似于图形验证码的校验,他是在Filter请求之前进行校验。 加了一个过滤器来在SmsAuthenticationFilter之前校验密码。
  2. 我们为什么不把短信验证码功能卸载SmsAuthentiationProvider里面,这是因为:验短信验证码的功能我们还希望给其他请求去使用;假如我们需要开发一个支付功能,每次支付完成前都需要做短信验证码校验,如果我们写在SmsAuthentiationProvider里面,那么短信验证码的功能是不能重复利用的:因为其包含额外短信验证码不需要的信息。

1.3 代码开发

因为短信登录功能无论在浏览器端还是手机端都会用,我们我们把他放到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();
    }
}

1.3.2 SmsCodeAuthenticationFilter开发

我们拷贝出: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;
    }
}

1.3.3 SmsCodeAuthenticationProvider 开发

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);
   }
}

1.3.4 SmsCodeFilter 开发

我们需要短信验证码拦截器:在请求进入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;
    }
}

测试

原文  https://segmentfault.com/a/1190000022050807
正文到此结束
Loading...