spring security_day3開發(fā)基于表單的驗(yàn)證

1. Spring Security簡(jiǎn)介

??Spring Security,是一種基于 SpringAOP 和 Servlet 過濾器的安全框架。它提供全面的安全性解決方案,同時(shí)在 Web 請(qǐng)求級(jí)和方法調(diào)用級(jí)處理身份確認(rèn)和授權(quán)。
??它主要包含以下三個(gè)功能:
??(1)認(rèn)證(你是誰?)。
??(2)授權(quán)(你可以做什么?)。
??(3)攻擊防護(hù)(防止偽造身份)。
??原理簡(jiǎn)述:如下圖所示,spring security會(huì)自動(dòng)創(chuàng)建一組過濾器鏈。其中綠色的過濾器用來根據(jù)請(qǐng)求參數(shù)封裝用戶的認(rèn)證信息,如果該請(qǐng)求包含某過濾器所需要的參數(shù),比如用戶名和密碼時(shí),該過濾器會(huì)獲取這些信息,封裝成對(duì)應(yīng)的認(rèn)證對(duì)象;藍(lán)色的過濾器會(huì)捕獲訪問的異常,當(dāng)對(duì)受保護(hù)的資源進(jìn)行訪問時(shí),如果線程中沒有對(duì)應(yīng)的認(rèn)證信息,spring security將會(huì)自動(dòng)使用匿名用戶對(duì)該資源進(jìn)行訪問,如果權(quán)限不夠則會(huì)拋出異常,而異常將會(huì)被捕獲,然后根據(jù)你的配置信息將會(huì)跳轉(zhuǎn)到認(rèn)證頁面進(jìn)行認(rèn)證;橘色的過濾器是資源的前一個(gè)過濾器,主要用作鑒權(quán),如果當(dāng)前用戶的權(quán)限不夠,將會(huì)拋出異常。

過濾器鏈
2. 自定義用戶認(rèn)證邏輯

??(1)自定義獲取用戶信息
??在之前的測(cè)試中,我們都是通過配置spring.security.user.name | password將用戶名和密碼寫死到文件中,我們?nèi)绾螐臄?shù)據(jù)庫中獲取用戶名和密碼并對(duì)表單中輸入的信息進(jìn)行驗(yàn)證呢?我們只需要實(shí)現(xiàn)UserDetailsService接口,并交由spring工廠管理即可。

@Component
public class MyUserDetailService implements UserDetailsService {

    private Logger logger = LoggerFactory.getLogger(getClass());
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        logger.info("從數(shù)據(jù)庫中查詢用戶名為'{}'的用戶信息",s);
        return new User(s, "123456",AuthorityUtils.createAuthorityList("USER"));
    }
}

??(2)處理用戶密碼的加解密
??用戶加密解密的類是PasswordEncoder,你可以實(shí)現(xiàn)該接口來定義自己的加解密邏輯。

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

??注意:就算相同的密碼加密之后的結(jié)果也是不相同的,如下所示都為"123456"加密后的結(jié)果

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

??因?yàn)檫@些加密后的字符串本身就包含加密所使使用到的鹽值的信息,這些鹽值是隨機(jī)生成的。在和原密碼進(jìn)行比對(duì)的過程中,spring security會(huì)將這些鹽值重新取出來按照相同的算法對(duì)表單輸入的密碼進(jìn)行加密,然后判斷是否和數(shù)據(jù)庫中加密后的字符串相等。

3. 個(gè)性化用戶認(rèn)證流程

??(1)自定義登錄頁面

@Configuration
public class BrowserSecurityConfig  extends WebSecurityConfigurerAdapter{

    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage("/imooc-sighIn.html")    //配置登錄頁面
                .loginProcessingUrl("/authentication/form") //自定義登錄處理url,默認(rèn)為 /login
                .and()
                .authorizeRequests()
                .antMatchers("/imooc-sighIn.html").permitAll()
                .anyRequest().authenticated()
                .and().csrf().disable();
    }
}

??(2)自定義登錄成功(失敗)處理
??SpringSecurity登錄成功會(huì)自動(dòng)跳轉(zhuǎn)到引發(fā)跳轉(zhuǎn)的頁面,我們可以實(shí)現(xiàn)AuthenticationSuccessHandler接口,實(shí)現(xiàn)自己的登錄成功的處理,在登錄成功后執(zhí)行自己的邏輯,比如用戶自動(dòng)簽到、用戶登錄日志等。

@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)
                ......
    }

??失敗處理器同成功處理器,需要實(shí)現(xiàn)AuthenticationFailureHandler接口,然后添加到spring security配置類中。

4. 用戶認(rèn)證流程流程

??核心類簡(jiǎn)介,我們首先看下我們的配置。

protected void configure(HttpSecurity http) throws Exception {
        String loginPage = securityProperties.getBrowser().getLoginPage();
        http.formLogin()
                .loginPage("/authentication/require")    //配置登錄頁面
                .loginProcessingUrl("/authentication/form") //自定義登錄處理url,默認(rèn)為 /login
                .successHandler(imoocAuthenticationSuccessHandler)
                .failureHandler(imoocAuthenticationFailureHandler)
                .and()
                .authorizeRequests()
                .antMatchers("/authentication/require", loginPage).permitAll()
                .anyRequest().authenticated()
                .and().csrf().disable();
    }
過濾器鏈

??在第一次訪問一個(gè)受保護(hù)的對(duì)象時(shí),請(qǐng)求經(jīng)過的過濾器鏈為SecurityContextPersistenceFilter -->AnonymousAuthenticationFilter --> ExceptionTranslationFilter --> FilterSecurityInterceptor。
??(1)SecurityContextPersistenceFilter過濾器的代碼如下,

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
         ......
        //1. 該過濾器會(huì)從session獲取SecurityContext,如果該SecurityContext為空,則創(chuàng)建一個(gè)空的SecurityContext
        HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
                response);
        SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
        try {
            //2. 將該SecurityContext放到線程中,然后執(zhí)行下一個(gè)過濾器
            SecurityContextHolder.setContext(contextBeforeChainExecution);
            chain.doFilter(holder.getRequest(), holder.getResponse());
        }
        finally {
            //3. 在返回響應(yīng)時(shí),因?yàn)榫€程池的原因,從線程中移除SecurityContext,如果session中沒有SecurityContext則將SecurityContext放到session中。
            SecurityContext contextAfterChainExecution = SecurityContextHolder
                    .getContext();
            SecurityContextHolder.clearContext();
            repo.saveContext(contextAfterChainExecution, holder.getRequest(),
                    holder.getResponse());
            request.removeAttribute(FILTER_APPLIED);
        }
    }

??(2)AnonymousAuthenticationFilter過濾器在所有的身份認(rèn)證過濾器的最后,當(dāng)經(jīng)過了所有的過濾器,線程中還是沒有認(rèn)證信息時(shí),該過濾器會(huì)往 SecurityContext 中增加一個(gè)匿名用戶的認(rèn)證信息。

  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過濾器就是用來捕獲鑒權(quán)過濾器拋出的異常,如果是未授權(quán)異常,則引導(dǎo)用戶跳轉(zhuǎn)到登錄頁面,代碼如下:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        ......
        try {
            //1. 該過濾器的下一個(gè)過濾器就是鑒權(quán)過濾器
            chain.doFilter(request, response);
        }
        catch (IOException ex) {
            throw ex;
        }
        catch (Exception ex) {
            // 根據(jù)捕獲的異常類型,做對(duì)應(yīng)的處理,比如認(rèn)證異常、訪問拒絕異常等等。如何處理,自己看。
        }
    }

??(4)FilterSecurityInterceptor,用戶鑒權(quán),判斷當(dāng)前認(rèn)證的用戶是否可以訪問該資源。如果無法訪問,拋出異常。

    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. 鑒權(quán)動(dòng)作
            InterceptorStatusToken token = super.beforeInvocation(fi);
            try {
           //2. 訪問受保護(hù)的資源
                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
            }
            finally {
                super.finallyInvocation(token);
            }
            super.afterInvocation(token, null);
        }
    }

    protected InterceptorStatusToken beforeInvocation(Object object) {
        ......
        //1. 首先從線程中獲取認(rèn)證信息,如果沒有認(rèn)證則是匿名用戶身份。
        Authentication authenticated = authenticateIfRequired();
        try {
            //2. 判斷當(dāng)前用戶是否可以訪問該資源,詳細(xì)之后再說
            this.accessDecisionManager.decide(authenticated, object, attributes);
        }
        catch (AccessDeniedException accessDeniedException) {
            throw accessDeniedException;
        }
        ......
    }

??由于第一次訪問的原因, 所以是由匿名用戶進(jìn)行訪問,會(huì)拋出訪問拒絕異常,此時(shí)ExceptionTranslationFilter會(huì)引導(dǎo)用戶跳轉(zhuǎn)到認(rèn)證頁面,當(dāng)用戶輸入完成認(rèn)證信息點(diǎn)擊登錄時(shí),請(qǐng)求再一次經(jīng)過這些攔截器鏈,不過不同的是,過濾器鏈中會(huì)新增一個(gè)過濾器,由于WebSecurityConfigurerAdapter配置類中的配置,提交表單(表單提交的地址是/authentication/form)的請(qǐng)求會(huì)被UsernamePasswordAuthenticationFilter過濾器攔截(默認(rèn)攔截/login請(qǐng)求)。所以如上圖所示,該請(qǐng)求的過濾器鏈中會(huì)新增一個(gè)用戶名密碼認(rèn)證過濾器。

??過濾器UsernamePasswordAuthenticationFilter的代碼如下:

1. 首先會(huì)調(diào)用父類的AbstractAuthenticationProcessingFilter的doFilter方法
      public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
         //1. 如果請(qǐng)求中沒有包含所需要的信息,比如用戶名、密碼,則跳過,進(jìn)入下一個(gè)過濾器
        if (!requiresAuthentication(request, response)) {
            chain.doFilter(request, response);
            return;
        }
        Authentication authResult;
        try {
             //2. 使用模板方法模式對(duì)用戶進(jìn)行認(rèn)證
            authResult = attemptAuthentication(request, response);
            if (authResult == null) {
                return;
            }
             //3. 木雞啊
            sessionStrategy.onAuthentication(authResult, request, response);
        }
        catch (InternalAuthenticationServiceException failed) {
            //4. 調(diào)用失敗處理器
            unsuccessfulAuthentication(request, response, failed);
            return;
        }
        //5. 將認(rèn)證信息放到線程中,然后調(diào)用成功處理器、調(diào)用rememberMeServiece服務(wù)等
        successfulAuthentication(request, response, chain, authResult);
    }

2. 調(diào)用UsernamePasswordAuthenticationFilter方法進(jìn)行封裝token,該類包含兩個(gè)構(gòu)造器
    public Authentication attemptAuthentication(HttpServletRequest request,
            HttpServletResponse response) throws AuthenticationException {
         //1. 從請(qǐng)求中獲取用戶名和密碼參數(shù)值
        String username = obtainUsername(request);
        String password = obtainPassword(request);
         //2. 根據(jù)用戶名和密碼封裝UsernamePasswordAuthenticationToken 對(duì)象
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                username, password);
         //3. 將請(qǐng)求的一些信息添加到token中 ,比如請(qǐng)求地址、sessionid等
        setDetails(request, authRequest);
        //4. 通過AuthenticationManager對(duì)象認(rèn)證token,并返回用戶認(rèn)證信息。
        return this.getAuthenticationManager().authenticate(authRequest);
    }

(1)UsernamePasswordAuthenticationToken包括兩個(gè)構(gòu)造方法,一種是已認(rèn)證、一種是未認(rèn)證
    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); 
    }

??在上述代碼的第四步的認(rèn)證過程中,會(huì)獲取當(dāng)前過濾器的認(rèn)證管理器AuthenticationManager,然后通過它進(jìn)行對(duì)token的認(rèn)證,而認(rèn)證管理器包含了多個(gè)認(rèn)證提供者AuthenticationProvider,認(rèn)證管理器實(shí)際上是使用它進(jìn)行身份認(rèn)證的,認(rèn)證管理器認(rèn)證的代碼如下。

    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        ......
        for (AuthenticationProvider provider : getProviders()) {
            //1.  從AuthenticationManagers 中獲取 可以處理該token類型的AuthenticationProvider
            if (!provider.supports(Authentication)) {
                continue;
            }
            //2. (用戶名和密碼方式使用的是DaoAuthenticationProvider)認(rèn)證管理器開始對(duì)token進(jìn)行驗(yàn)證
            try {
                result = provider.authenticate(authentication);
                if (result != null) {
                    copyDetails(authentication, result);
                    break;
                }
            }
            catch (AccountStatusException e) {
                throw e
            }
        }
           //3. 如果該認(rèn)證管理器中沒有可處理的提供者,交由父管理器處理
        if (result == null && parent != null) {
            try {
                result = parent.authenticate(authentication);
            }
             ......
        }

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

        // 4. 如果沒有可以處理的該token類型的提供者,則拋出異常
        ......
        throw lastException;
    }

??選取到可以處理該token類型的認(rèn)證提供者后,就開始處理該token。DaoAuthenticationProvider的處理過程如下:

1. 首先調(diào)用父類AbstractUserDetailsAuthenticationProvider的authenticate方法
public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        //1. 獲取token的憑證,也就是表單的用戶名
        String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
                : authentication.getName();

        boolean cacheWasUsed = true;
         //2. 從緩存中根據(jù)用戶名獲得UserDetails 
        UserDetails user = this.userCache.getUserFromCache(username);

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

            try {
                //3. 調(diào)用DaoAuthenticationProvider的retrieveUser方法進(jìn)行處理
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            catch (UsernameNotFoundException notFound) {}
        }
        //4. 檢測(cè)用戶是否有效,比如user的賬號(hào)是否凍結(jié)、是否過期等,user密碼是否和認(rèn)證信息中的密碼一致(表單中輸入的密碼),如果無效拋出異常
        ......代碼太長(zhǎng)略
        if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }

        Object principalToReturn = user;

        if (forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }
        //5.  根據(jù)認(rèn)證信息重新封裝token
        return createSuccessAuthentication(principalToReturn, authentication, user);
    }
    //認(rèn)證成功之后,調(diào)用第二個(gè)構(gòu)造器進(jìn)行構(gòu)造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. 在上個(gè)方法的第二步中,調(diào)用的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;
    }

??認(rèn)證的整體流程如下所示:

認(rèn)證完整流程
5. 記住我功能
  1. 記住我基本原理
rememberMe基本原理

??在AbstractAuthenticationProcessingFilter類的方法doFilter()方法認(rèn)證完成之后,會(huì)調(diào)用successfulAuthentication方法,如下所示,該方法主要做了三件事:將認(rèn)證的用戶信息放入線程;調(diào)用記住我服務(wù)(默認(rèn)是NullRememberMeServices,什么都不做);調(diào)用成功處理器。

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

??如上圖所示,如果配置了記住我功能,在對(duì)用戶認(rèn)證完成之后,會(huì)先將認(rèn)證后的token持久化到介質(zhì)中(可以是數(shù)據(jù)庫、session等),然后再將該token寫入到瀏覽器的cookie中。當(dāng)下次請(qǐng)求到來時(shí)如果沒有過濾器可以從請(qǐng)求中獲取認(rèn)證信息時(shí),則嘗試使用該記住我過濾器從持久化的介質(zhì)中讀取該token信息,然后根據(jù)token的信息使用自定義的UserDetailServcie進(jìn)行登錄流程,登錄完成之后將該認(rèn)證信息放到線程中,如果所有的瀏覽器都無法獲取認(rèn)證信息,則使用匿名身份進(jìn)行訪問。該過濾器的位置如下所示。

rememberMe過濾器鏈的位置
  1. 記住我基本實(shí)現(xiàn)
1. 在表單中添加一個(gè)單選框,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);  //在項(xiàng)目啟動(dòng)時(shí),是否創(chuàng)建表。也可以手動(dòng)執(zhí)行,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. 記住我源碼解析
    ??在用戶登錄完成之后,會(huì)調(diào)用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. 然后會(huì)調(diào)用記住我服務(wù)(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持久化到數(shù)據(jù)庫中
            tokenRepository.createNewToken(persistentToken);
              //3. 將該token添加到cookie中
            addCookie(persistentToken, request, response);
        }
        catch (Exception e) {
            logger.error("Failed to save persistent token ", e);
        }
    }

??在登錄完成之后,下次請(qǐng)求到來時(shí),會(huì)被RememberMeAuthenticationFilter過濾器進(jìn)行攔截。

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
            //1. 說明該過濾器之前沒有過濾器可以從請(qǐng)求中獲取認(rèn)證信息。
        if (SecurityContextHolder.getContext().getAuthentication() == null) {
             //2. 嘗試使用cookie中的信息從數(shù)據(jù)庫中獲取認(rèn)證信息
            Authentication rememberMeAuth = rememberMeServices.autoLogin(request,
                    response);

            if (rememberMeAuth != null) {
                try {
                     //3.  根據(jù)使用認(rèn)證管理器對(duì)認(rèn)證信息進(jìn)行認(rèn)證,認(rèn)證成功后將認(rèn)證信息放入線程
                    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.  調(diào)用認(rèn)證服務(wù)的失敗處理
                    rememberMeServices.loginFail(request, response);
                    onUnsuccessfulAuthentication(request, response,
                            authenticationException);
                }
            }
            chain.doFilter(request, response);
        }
        else {
            //如果從之前的過濾器已經(jīng)獲取到認(rèn)證信息,則進(jìn)入下一個(gè)過濾器
            chain.doFilter(request, response);
        }
    }

??上述代碼的第二步從數(shù)據(jù)庫中獲取用戶認(rèn)證信息的代碼如下所示:

public final Authentication autoLogin(HttpServletRequest request,
            HttpServletResponse response) {
      //1. 首先從請(qǐng)求的cookie中獲取rememberMe的token的相關(guān)信息(比如token在數(shù)據(jù)庫的唯一標(biāo)識(shí))
        String rememberMeCookie = extractRememberMeCookie(request);
        UserDetails user = null;
        try {
            String[] cookieTokens = decodeCookie(rememberMeCookie);
             //2. 從數(shù)據(jù)庫中獲取記住我的用戶信息
            user = processAutoLoginCookie(cookieTokens, request, response);
            userDetailsChecker.check(user);
            logger.debug("Remember-me cookie accepted");
          //3. 構(gòu)建RememberMeAuthenticationToken類型的認(rèn)證對(duì)象
            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. 從數(shù)據(jù)庫中查詢用戶信息
        PersistentRememberMeToken token = tokenRepository
                .getTokenForSeries(presentedSeries);
      //2. 根據(jù)用戶信息token封裝一個(gè)新的token
        PersistentRememberMeToken newToken = new PersistentRememberMeToken(
                token.getUsername(), token.getSeries(), generateTokenData(), new Date());

        try {
              //3. 將新token更新到數(shù)據(jù)庫中
            tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(),
                    newToken.getDate());
            addCookie(newToken, request, response);
        }
          //4. 調(diào)用自定義的UserDetailService進(jìn)行查詢用戶
        return getUserDetailsService().loadUserByUsername(token.getUsername());
    }

6. 實(shí)戰(zhàn)開發(fā)手機(jī)登錄

??在上邊看過UsernamePasswordAuthenticationFilter的執(zhí)行流程之后,我們開始根據(jù)用戶名和密碼驗(yàn)證的方式進(jìn)行開發(fā)。

驗(yàn)證所需要的類

??由上圖可知,我們構(gòu)建認(rèn)證用戶信息需要三個(gè)類:從請(qǐng)求中獲取用戶信息的SmsAuthenticationFilter;驗(yàn)證并構(gòu)建認(rèn)證信息的SmsAuthenticationProvider;封裝的手機(jī)號(hào)的認(rèn)證信息的類SmsAuthenticationToken。在這該SmsAuthenticationFilter過濾器之前,應(yīng)該還需要驗(yàn)證手機(jī)的驗(yàn)證碼是否正確的過濾器。所以總共需要四個(gè)類。
??(1)驗(yàn)證手機(jī)號(hào)是否正確的過濾器SmsCheckFilter

public class SmsCheckFilter extends OncePerRequestFilter {

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

        if(表單提交的驗(yàn)證碼.equals(  session中的驗(yàn)證碼  ))
            filterChain.doFilter(httpServletRequest, httpServletResponse);
        }else{
            驗(yàn)證碼錯(cuò)誤的失敗處理?。?!
        }
    }
}

??(2)從請(qǐng)求中獲取用戶信息的過濾器SmsCodeAuthenticationFilter(仿照UsernamePasswordAuthenticationFilter)。

public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    .....

    public SmsCodeAuthenticationFilter() {
        super(new AntPathRequestMatcher( "手機(jī)登錄表單提交的url", "POST"));
    }


    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {
        //1. 從請(qǐng)求中獲取用戶登錄所使用的手機(jī)號(hào)
        String mobile = obtainMobile(request);
        //2. 使用手機(jī)號(hào)封裝認(rèn)證信息
        SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
      //3. 將請(qǐng)求信息附加到token中
        setDetails(request, authRequest);
        // 4. 開始對(duì)token進(jìn)行驗(yàn)證
        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)驗(yàn)證并構(gòu)建已認(rèn)證的認(rèn)證信息的類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("手機(jī)號(hào)未綁定??!");
        }
        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)將這以上所有類聯(lián)系起來,并添加到配置中

@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. 認(rèn)證時(shí)使用該UserDetailService進(jìn)行查詢手機(jī)號(hào)對(duì)應(yīng)的用戶
        smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
        AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);

        //2. 將spring security的認(rèn)證管理器添加到自定義過濾器中,用來篩選合適的認(rèn)證器對(duì)token進(jìn)行驗(yàn)證
        smsCodeAuthenticationFilter.setAuthenticationManager(authenticationManager);

        //3. 將認(rèn)證提供者添加到認(rèn)證管理器中,并將該自定義過濾器添加到過濾器鏈中
        http.authenticationProvider(smsCodeAuthenticationProvider).addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

將以上配置和核對(duì)手機(jī)號(hào)的過濾器添加到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);
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容