spring security_day3开发基于表单的验证

1. Spring Security简介

  Spring Security,是一种基于 SpringAOP 和 Servlet 过滤器的安全框架。它提供全面的安全性解决方案,同时在 Web 请求级和方法调用级处理身份确认和授权。
  它主要包含以下三个功能:
  (1)认证(你是谁?)。
  (2)授权(你可以做什么?)。
  (3)攻击防护(防止伪造身份)。
  原理简述:如下图所示,spring security会自动创建一组过滤器链。其中绿色的过滤器用来根据请求参数封装用户的认证信息,如果该请求包含某过滤器所需要的参数,比如用户名和密码时,该过滤器会获取这些信息,封装成对应的认证对象;蓝色的过滤器会捕获访问的异常,当对受保护的资源进行访问时,如果线程中没有对应的认证信息,spring security将会自动使用匿名用户对该资源进行访问,如果权限不够则会抛出异常,而异常将会被捕获,然后根据你的配置信息将会跳转到认证页面进行认证;橘色的过滤器是资源的前一个过滤器,主要用作鉴权,如果当前用户的权限不够,将会抛出异常。

过滤器链
2. 自定义用户认证逻辑

  (1)自定义获取用户信息
  在之前的测试中,我们都是通过配置spring.security.user.name | password将用户名和密码写死到文件中,我们如何从数据库中获取用户名和密码并对表单中输入的信息进行验证呢?我们只需要实现UserDetailsService接口,并交由spring工厂管理即可。

@Component
public class MyUserDetailService implements UserDetailsService {

    private Logger logger = LoggerFactory.getLogger(getClass());
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        logger.info("从数据库中查询用户名为'{}'的用户信息",s);
        return new User(s, "123456",AuthorityUtils.createAuthorityList("USER"));
    }
}

  (2)处理用户密码的加解密
  用户加密解密的类是PasswordEncoder,你可以实现该接口来定义自己的加解密逻辑。

   @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

  注意:就算相同的密码加密之后的结果也是不相同的,如下所示都为"123456"加密后的结果

$2a$10$eQ5pNpNPDnvMvOs1w5Xz9.hdjwdDSFw7kmXIPcpvsxgkOIcDigmqu
$2a$10$aYTOucDNIAweM8sHyiEVye40761Nz4sdiizncSOBA.39xdxdKjZBO

  因为这些加密后的字符串本身就包含加密所使使用到的盐值的信息,这些盐值是随机生成的。在和原密码进行比对的过程中,spring security会将这些盐值重新取出来按照相同的算法对表单输入的密码进行加密,然后判断是否和数据库中加密后的字符串相等。

3. 个性化用户认证流程

  (1)自定义登录页面

@Configuration
public class BrowserSecurityConfig  extends WebSecurityConfigurerAdapter{

    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage("/imooc-sighIn.html")    //配置登录页面
                .loginProcessingUrl("/authentication/form") //自定义登录处理url,默认为 /login
                .and()
                .authorizeRequests()
                .antMatchers("/imooc-sighIn.html").permitAll()
                .anyRequest().authenticated()
                .and().csrf().disable();
    }
}

  (2)自定义登录成功(失败)处理
  SpringSecurity登录成功会自动跳转到引发跳转的页面,我们可以实现AuthenticationSuccessHandler接口,实现自己的登录成功的处理,在登录成功后执行自己的逻辑,比如用户自动签到、用户登录日志等。

@Component
public class ImoocAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private ObjectMapper objectMapper ;
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        logger.info("用户{}签到成功!", authentication.getName());
        response.setContentType("application/json;charSet=UTF-8");
        PrintWriter writer = response.getWriter();
        writer.write(objectMapper.writeValueAsString(authentication));
        writer.flush();
    }
}

@Configuration
public class BrowserSecurityConfig  extends WebSecurityConfigurerAdapter{
    @Autowired
    private AuthenticationSuccessHandler imoocAuthenticationSuccessHandler;

    protected void configure(HttpSecurity http) throws Exception {
   
        http.formLogin()
                ......
                .successHandler(imoocAuthenticationSuccessHandler)
                ......
    }

  失败处理器同成功处理器,需要实现AuthenticationFailureHandler接口,然后添加到spring security配置类中。

4. 用户认证流程流程

  核心类简介,我们首先看下我们的配置。

protected void configure(HttpSecurity http) throws Exception {
        String loginPage = securityProperties.getBrowser().getLoginPage();
        http.formLogin()
                .loginPage("/authentication/require")    //配置登录页面
                .loginProcessingUrl("/authentication/form") //自定义登录处理url,默认为 /login
                .successHandler(imoocAuthenticationSuccessHandler)
                .failureHandler(imoocAuthenticationFailureHandler)
                .and()
                .authorizeRequests()
                .antMatchers("/authentication/require", loginPage).permitAll()
                .anyRequest().authenticated()
                .and().csrf().disable();
    }
过滤器链

  在第一次访问一个受保护的对象时,请求经过的过滤器链为SecurityContextPersistenceFilter -->AnonymousAuthenticationFilter --> ExceptionTranslationFilter --> FilterSecurityInterceptor
  (1)SecurityContextPersistenceFilter过滤器的代码如下,

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
         ......
        //1. 该过滤器会从session获取SecurityContext,如果该SecurityContext为空,则创建一个空的SecurityContext
        HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
                response);
        SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
        try {
            //2. 将该SecurityContext放到线程中,然后执行下一个过滤器
            SecurityContextHolder.setContext(contextBeforeChainExecution);
            chain.doFilter(holder.getRequest(), holder.getResponse());
        }
        finally {
            //3. 在返回响应时,因为线程池的原因,从线程中移除SecurityContext,如果session中没有SecurityContext则将SecurityContext放到session中。
            SecurityContext contextAfterChainExecution = SecurityContextHolder
                    .getContext();
            SecurityContextHolder.clearContext();
            repo.saveContext(contextAfterChainExecution, holder.getRequest(),
                    holder.getResponse());
            request.removeAttribute(FILTER_APPLIED);
        }
    }

  (2)AnonymousAuthenticationFilter过滤器在所有的身份认证过滤器的最后,当经过了所有的过滤器,线程中还是没有认证信息时,该过滤器会往 SecurityContext 中增加一个匿名用户的认证信息。

  public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        if (SecurityContextHolder.getContext().getAuthentication() == null) {
            SecurityContextHolder.getContext().setAuthentication(
                    createAuthentication((HttpServletRequest) req));
        }
        chain.doFilter(req, res);
    }

   protected Authentication createAuthentication(HttpServletRequest request) {
        AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key,
                principal, authorities);
        auth.setDetails(authenticationDetailsSource.buildDetails(request));

        return auth;
    }

  (3)ExceptionTranslationFilter过滤器就是用来捕获鉴权过滤器抛出的异常,如果是未授权异常,则引导用户跳转到登录页面,代码如下:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        ......
        try {
            //1. 该过滤器的下一个过滤器就是鉴权过滤器
            chain.doFilter(request, response);
        }
        catch (IOException ex) {
            throw ex;
        }
        catch (Exception ex) {
            // 根据捕获的异常类型,做对应的处理,比如认证异常、访问拒绝异常等等。如何处理,自己看。
        }
    }

  (4)FilterSecurityInterceptor,用户鉴权,判断当前认证的用户是否可以访问该资源。如果无法访问,抛出异常。

    public void invoke(FilterInvocation fi) throws IOException, ServletException {
        if ((fi.getRequest() != null)
                && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
                && observeOncePerRequest) {
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        }
        else {
           //1. 鉴权动作
            InterceptorStatusToken token = super.beforeInvocation(fi);
            try {
           //2. 访问受保护的资源
                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
            }
            finally {
                super.finallyInvocation(token);
            }
            super.afterInvocation(token, null);
        }
    }

    protected InterceptorStatusToken beforeInvocation(Object object) {
        ......
        //1. 首先从线程中获取认证信息,如果没有认证则是匿名用户身份。
        Authentication authenticated = authenticateIfRequired();
        try {
            //2. 判断当前用户是否可以访问该资源,详细之后再说
            this.accessDecisionManager.decide(authenticated, object, attributes);
        }
        catch (AccessDeniedException accessDeniedException) {
            throw accessDeniedException;
        }
        ......
    }

  由于第一次访问的原因, 所以是由匿名用户进行访问,会抛出访问拒绝异常,此时ExceptionTranslationFilter会引导用户跳转到认证页面,当用户输入完成认证信息点击登录时,请求再一次经过这些拦截器链,不过不同的是,过滤器链中会新增一个过滤器,由于WebSecurityConfigurerAdapter配置类中的配置,提交表单(表单提交的地址是/authentication/form)的请求会被UsernamePasswordAuthenticationFilter过滤器拦截(默认拦截/login请求)。所以如上图所示,该请求的过滤器链中会新增一个用户名密码认证过滤器。

  过滤器UsernamePasswordAuthenticationFilter的代码如下:

1. 首先会调用父类的AbstractAuthenticationProcessingFilter的doFilter方法
      public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
         //1. 如果请求中没有包含所需要的信息,比如用户名、密码,则跳过,进入下一个过滤器
        if (!requiresAuthentication(request, response)) {
            chain.doFilter(request, response);
            return;
        }
        Authentication authResult;
        try {
             //2. 使用模板方法模式对用户进行认证
            authResult = attemptAuthentication(request, response);
            if (authResult == null) {
                return;
            }
             //3. 木鸡啊
            sessionStrategy.onAuthentication(authResult, request, response);
        }
        catch (InternalAuthenticationServiceException failed) {
            //4. 调用失败处理器
            unsuccessfulAuthentication(request, response, failed);
            return;
        }
        //5. 将认证信息放到线程中,然后调用成功处理器、调用rememberMeServiece服务等
        successfulAuthentication(request, response, chain, authResult);
    }

2. 调用UsernamePasswordAuthenticationFilter方法进行封装token,该类包含两个构造器
    public Authentication attemptAuthentication(HttpServletRequest request,
            HttpServletResponse response) throws AuthenticationException {
         //1. 从请求中获取用户名和密码参数值
        String username = obtainUsername(request);
        String password = obtainPassword(request);
         //2. 根据用户名和密码封装UsernamePasswordAuthenticationToken 对象
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                username, password);
         //3. 将请求的一些信息添加到token中 ,比如请求地址、sessionid等
        setDetails(request, authRequest);
        //4. 通过AuthenticationManager对象认证token,并返回用户认证信息。
        return this.getAuthenticationManager().authenticate(authRequest);
    }

(1)UsernamePasswordAuthenticationToken包括两个构造方法,一种是已认证、一种是未认证
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        setAuthenticated(false);      //look here !!!!
    }
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
            Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true); 
    }

  在上述代码的第四步的认证过程中,会获取当前过滤器的认证管理器AuthenticationManager,然后通过它进行对token的认证,而认证管理器包含了多个认证提供者AuthenticationProvider,认证管理器实际上是使用它进行身份认证的,认证管理器认证的代码如下。

    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        ......
        for (AuthenticationProvider provider : getProviders()) {
            //1.  从AuthenticationManagers 中获取 可以处理该token类型的AuthenticationProvider
            if (!provider.supports(Authentication)) {
                continue;
            }
            //2. (用户名和密码方式使用的是DaoAuthenticationProvider)认证管理器开始对token进行验证
            try {
                result = provider.authenticate(authentication);
                if (result != null) {
                    copyDetails(authentication, result);
                    break;
                }
            }
            catch (AccountStatusException e) {
                throw e
            }
        }
           //3. 如果该认证管理器中没有可处理的提供者,交由父管理器处理
        if (result == null && parent != null) {
            try {
                result = parent.authenticate(authentication);
            }
             ......
        }

        if (result != null) {
            eventPublisher.publishAuthenticationSuccess(result);
            return result;
        }

        // 4. 如果没有可以处理的该token类型的提供者,则抛出异常
        ......
        throw lastException;
    }

  选取到可以处理该token类型的认证提供者后,就开始处理该token。DaoAuthenticationProvider的处理过程如下:

1. 首先调用父类AbstractUserDetailsAuthenticationProvider的authenticate方法
public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        //1. 获取token的凭证,也就是表单的用户名
        String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
                : authentication.getName();

        boolean cacheWasUsed = true;
         //2. 从缓存中根据用户名获得UserDetails 
        UserDetails user = this.userCache.getUserFromCache(username);

        if (user == null) {
            cacheWasUsed = false;

            try {
                //3. 调用DaoAuthenticationProvider的retrieveUser方法进行处理
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            catch (UsernameNotFoundException notFound) {}
        }
        //4. 检测用户是否有效,比如user的账号是否冻结、是否过期等,user密码是否和认证信息中的密码一致(表单中输入的密码),如果无效抛出异常
        ......代码太长略
        if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }

        Object principalToReturn = user;

        if (forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }
        //5.  根据认证信息重新封装token
        return createSuccessAuthentication(principalToReturn, authentication, user);
    }
    //认证成功之后,调用第二个构造器进行构造UsernamePasswordAuthenticationToken
    protected Authentication createSuccessAuthentication(Object principal,
            Authentication authentication, UserDetails user) {
        UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
                principal, authentication.getCredentials(),
                authoritiesMapper.mapAuthorities(user.getAuthorities()));
        result.setDetails(authentication.getDetails());

        return result;
    }

2. 在上个方法的第二步中,调用的retrieveUser方法如下所示
    protected final UserDetails retrieveUser(String username,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        UserDetails loadedUser;
        try {
            //使用你自定义的UserDetailsService获取用户信息
            loadedUser = this.getUserDetailsService().loadUserByUsername(username);
        }
        catch (UsernameNotFoundException notFound) {
            throw notFound;
        }
        return loadedUser;
    }

  认证的整体流程如下所示:

认证完整流程
5. 记住我功能
  1. 记住我基本原理
rememberMe基本原理

  在AbstractAuthenticationProcessingFilter类的方法doFilter()方法认证完成之后,会调用successfulAuthentication方法,如下所示,该方法主要做了三件事:将认证的用户信息放入线程;调用记住我服务(默认是NullRememberMeServices,什么都不做);调用成功处理器。

protected void successfulAuthentication(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain, Authentication authResult)
            throws IOException, ServletException {

        SecurityContextHolder.getContext().setAuthentication(authResult);
        rememberMeServices.loginSuccess(request, response, authResult);
        successHandler.onAuthenticationSuccess(request, response, authResult);
    }

  如上图所示,如果配置了记住我功能,在对用户认证完成之后,会先将认证后的token持久化到介质中(可以是数据库、session等),然后再将该token写入到浏览器的cookie中。当下次请求到来时如果没有过滤器可以从请求中获取认证信息时,则尝试使用该记住我过滤器从持久化的介质中读取该token信息,然后根据token的信息使用自定义的UserDetailServcie进行登录流程,登录完成之后将该认证信息放到线程中,如果所有的浏览器都无法获取认证信息,则使用匿名身份进行访问。该过滤器的位置如下所示。

rememberMe过滤器链的位置
  1. 记住我基本实现
1. 在表单中添加一个单选框,name属性必须为 "remember-me",也可以在配置中指定
<input type="checkbox" name="remember-me" value="true"/>记住我
2. 指定token的持久化方式
  @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        jdbcTokenRepository.setCreateTableOnStartup(true);  //在项目启动时,是否创建表。也可以手动执行,sql语句在该类的CREATE_TABLE_SQL属性。
        return jdbcTokenRepository;
    }
3. 配置rememberMe的功能
@Configuration
public class BrowserSecurityConfig  extends WebSecurityConfigurerAdapter{
    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private AuthenticationSuccessHandler imoocAuthenticationSuccessHandler;
    @Autowired

    private AuthenticationFailureHandler imoocAuthenticationFailureHandler;

    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                ......
                .and()
            .rememberMe()
                .tokenRepository(persistentTokenRepository())
                .tokenValiditySeconds(30)
                .userDetailsService(userDetailsService)
                .....
    }
  1. 记住我源码解析
      在用户登录完成之后,会调用AbstractAuthenticationProcessingFilter类的successfulAuthentication方法,如下所示:
    1. 用户登录完成的处理
    protected void successfulAuthentication(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain, Authentication authResult)
            throws IOException, ServletException {

        SecurityContextHolder.getContext().setAuthentication(authResult);
        rememberMeServices.loginSuccess(request, response, authResult);
        successHandler.onAuthenticationSuccess(request, response, authResult);
    }

    2. 然后会调用记住我服务(PersistentTokenBasedRememberMeServices类)的loginSuccess方法
protected void onLoginSuccess(HttpServletRequest request,
            HttpServletResponse response, Authentication successfulAuthentication) {
        String username = successfulAuthentication.getName();
        //1. 将用户信息封装成持久化的token
        PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
                username, generateSeriesData(), generateTokenData(), new Date());
        try {
              //2. 将该token持久化到数据库中
            tokenRepository.createNewToken(persistentToken);
              //3. 将该token添加到cookie中
            addCookie(persistentToken, request, response);
        }
        catch (Exception e) {
            logger.error("Failed to save persistent token ", e);
        }
    }

  在登录完成之后,下次请求到来时,会被RememberMeAuthenticationFilter过滤器进行拦截。

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
            //1. 说明该过滤器之前没有过滤器可以从请求中获取认证信息。
        if (SecurityContextHolder.getContext().getAuthentication() == null) {
             //2. 尝试使用cookie中的信息从数据库中获取认证信息
            Authentication rememberMeAuth = rememberMeServices.autoLogin(request,
                    response);

            if (rememberMeAuth != null) {
                try {
                     //3.  根据使用认证管理器对认证信息进行认证,认证成功后将认证信息放入线程
                    rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);
                    SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);

                    onSuccessfulAuthentication(request, response, rememberMeAuth);
                    if (successHandler != null) {
                        successHandler.onAuthenticationSuccess(request, response,
                                rememberMeAuth);
                        return;
                    }
                }
                catch (AuthenticationException authenticationException) {
                    //4.  调用认证服务的失败处理
                    rememberMeServices.loginFail(request, response);
                    onUnsuccessfulAuthentication(request, response,
                            authenticationException);
                }
            }
            chain.doFilter(request, response);
        }
        else {
            //如果从之前的过滤器已经获取到认证信息,则进入下一个过滤器
            chain.doFilter(request, response);
        }
    }

  上述代码的第二步从数据库中获取用户认证信息的代码如下所示:

public final Authentication autoLogin(HttpServletRequest request,
            HttpServletResponse response) {
      //1. 首先从请求的cookie中获取rememberMe的token的相关信息(比如token在数据库的唯一标识)
        String rememberMeCookie = extractRememberMeCookie(request);
        UserDetails user = null;
        try {
            String[] cookieTokens = decodeCookie(rememberMeCookie);
             //2. 从数据库中获取记住我的用户信息
            user = processAutoLoginCookie(cookieTokens, request, response);
            userDetailsChecker.check(user);
            logger.debug("Remember-me cookie accepted");
          //3. 构建RememberMeAuthenticationToken类型的认证对象
            return createSuccessfulAuthentication(request, user);
        }
        catch (Exception cte) {
            throw cte;
        }
    }

    protected UserDetails processAutoLoginCookie(String[] cookieTokens,
            HttpServletRequest request, HttpServletResponse response) {

        final String presentedSeries = cookieTokens[0];
        final String presentedToken = cookieTokens[1];
        //1. 从数据库中查询用户信息
        PersistentRememberMeToken token = tokenRepository
                .getTokenForSeries(presentedSeries);
      //2. 根据用户信息token封装一个新的token
        PersistentRememberMeToken newToken = new PersistentRememberMeToken(
                token.getUsername(), token.getSeries(), generateTokenData(), new Date());

        try {
              //3. 将新token更新到数据库中
            tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(),
                    newToken.getDate());
            addCookie(newToken, request, response);
        }
          //4. 调用自定义的UserDetailService进行查询用户
        return getUserDetailsService().loadUserByUsername(token.getUsername());
    }

6. 实战开发手机登录

  在上边看过UsernamePasswordAuthenticationFilter的执行流程之后,我们开始根据用户名和密码验证的方式进行开发。

验证所需要的类

  由上图可知,我们构建认证用户信息需要三个类:从请求中获取用户信息的SmsAuthenticationFilter;验证并构建认证信息的SmsAuthenticationProvider;封装的手机号的认证信息的类SmsAuthenticationToken。在这该SmsAuthenticationFilter过滤器之前,应该还需要验证手机的验证码是否正确的过滤器。所以总共需要四个类。
  (1)验证手机号是否正确的过滤器SmsCheckFilter

public class SmsCheckFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {

        if(表单提交的验证码.equals(  session中的验证码  ))
            filterChain.doFilter(httpServletRequest, httpServletResponse);
        }else{
            验证码错误的失败处理!!!
        }
    }
}

  (2)从请求中获取用户信息的过滤器SmsCodeAuthenticationFilter(仿照UsernamePasswordAuthenticationFilter)。

public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    .....

    public SmsCodeAuthenticationFilter() {
        super(new AntPathRequestMatcher( "手机登录表单提交的url", "POST"));
    }


    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {
        //1. 从请求中获取用户登录所使用的手机号
        String mobile = obtainMobile(request);
        //2. 使用手机号封装认证信息
        SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
      //3. 将请求信息附加到token中
        setDetails(request, authRequest);
        // 4. 开始对token进行验证
        return this.getAuthenticationManager().authenticate(authRequest);
    }
    ......
}

  (3)封装用户信息的tokenSmsCodeAuthenticationToken(仿照UsernamePasswordAuthenticationToken)

public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    private  Object mobile;

    public SmsCodeAuthenticationToken(Object mobile) {
        super(null);
        this.mobile = mobile;
        setAuthenticated(false);
    }

    public SmsCodeAuthenticationToken(Object principal,
                                               Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.mobile = principal;
        super.setAuthenticated(true); // must use super, as we override
    }

    public Object getCredentials() {
        return null;
    }

    public Object getPrincipal() {
        return this.mobile;
    }

    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
    }
}

  (4)验证并构建已认证的认证信息的类SmsCodeAuthenticationProvider

public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    public UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    private UserDetailsService userDetailsService;
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Object principal = authentication.getPrincipal();
        UserDetails userDetails = userDetailsService.loadUserByUsername(principal.toString());
        if(userDetails == null){
            throw  new RuntimeException("手机号未绑定!!");
        }
        SmsCodeAuthenticationToken smsCodeAuthenticationToken = new SmsCodeAuthenticationToken(principal, userDetails.getAuthorities());
        smsCodeAuthenticationToken.setDetails(authentication.getDetails());
        return smsCodeAuthenticationToken;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

  (5)将这以上所有类联系起来,并添加到配置中

@Component
public class SmsCodeFilterConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    @Autowired
    private UserDetailsService userDetailsService;
    @Override
    public void configure(HttpSecurity http) throws Exception {
        SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        //1. 认证时使用该UserDetailService进行查询手机号对应的用户
        smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
        AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);

        //2. 将spring security的认证管理器添加到自定义过滤器中,用来筛选合适的认证器对token进行验证
        smsCodeAuthenticationFilter.setAuthenticationManager(authenticationManager);

        //3. 将认证提供者添加到认证管理器中,并将该自定义过滤器添加到过滤器链中
        http.authenticationProvider(smsCodeAuthenticationProvider).addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

将以上配置和核对手机号的过滤器添加到spring security的配置中。
@Configuration
public class BrowserSecurityConfig  extends WebSecurityConfigurerAdapter{
    protected void configure(HttpSecurity http) throws Exception {
        String loginPage = securityProperties.getBrowser().getLoginPage();
        http.addFilterBefore(new SmsCheckFilter(), UsernamePasswordAuthenticationFilter.class)
          .....
        .apply(smsCodeFilterConfig);
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容