【SpringSecurity系列02】SpringSecurity 表單認證邏輯源碼解讀

概要

前面一節,經過簡單配置便可實現SpringSecurity表單認證功能,而今天這一節將經過閱讀源碼的形式來學習SpringSecurity是如何實現這些功能, 前方高能預警,本篇分析源碼篇幅較長java

<!-- more -->數據庫

過濾器鏈

前面我說過SpringSecurity是基於過濾器鏈的形式,那麼我解析將會介紹一下具體有哪些過濾器。設計模式

Filter Class 介紹
SecurityContextPersistenceFilter 判斷當前用戶是否登陸
CrsfFilter 用於防止csrf攻擊
LogoutFilter 處理註銷請求
UsernamePasswordAuthenticationFilter 處理表單登陸的請求(也是咱們今天的主角)
BasicAuthenticationFilter 處理http basic認證的請求

因爲過濾器鏈中的過濾器實在太多,我沒有一一列舉,調了幾個比較重要的介紹一下。緩存

經過上面咱們知道SpringSecurity對於表單登陸的認證請求是交給了UsernamePasswordAuthenticationFilter處理的,那麼具體的認證流程以下:cookie

Spring Security ç™"录流程.jpg

從上圖可知,UsernamePasswordAuthenticationFilter繼承於抽象類AbstractAuthenticationProcessingFiltersession

具體認證是:app

  1. 進入doFilter方法,判斷是否要認證,若是須要認證則進入attemptAuthentication方法,若是不須要直接結束
  2. attemptAuthentication方法中根據username跟password構造一個UsernamePasswordAuthenticationToken對象(此時的token是未認證的),而且將它交給ProviderManger來完成認證。
  3. ProviderManger中維護這一個AuthenticationProvider對象列表,經過遍歷判斷而且最後選擇DaoAuthenticationProvider對象來完成最後的認證。
  4. DaoAuthenticationProvider根據ProviderManger傳來的token取出username,而且調用咱們寫的UserDetailsService的loadUserByUsername方法從數據庫中讀取用戶信息,而後對比用戶密碼,若是認證經過,則返回用戶信息也是就是UserDetails對象,在從新構造UsernamePasswordAuthenticationToken(此時的token是 已經認證經過了的)。

接下來咱們將經過源碼來分析具體的整個認證流程。ide

AbstractAuthenticationProcessingFilter

AbstractAuthenticationProcessingFilter 是一個抽象類。全部的認證認證請求的過濾器都會繼承於它,它主要將一些公共的功能實現,而具體的驗證邏輯交給子類實現,有點相似於父類設置好認證流程,子類負責具體的認證邏輯,這樣跟設計模式的模板方法模式有點類似。函數

如今咱們分析一下 它裏面比較重要的方法post

一、doFilter

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        // 省略不相干代碼。。。
    // 一、判斷當前請求是否要認證
        if (!requiresAuthentication(request, response)) {
      // 不須要直接走下一個過濾器
            chain.doFilter(request, response);
            return;
        }
        try {
      // 二、開始請求認證,attemptAuthentication具體實現給子類,若是認證成功返回一個認證經過的Authenticaion對象
            authResult = attemptAuthentication(request, response);
            if (authResult == null) {
                return;
            }
      // 三、登陸成功 將認證成功的用戶信息放入session SessionAuthenticationStrategy接口,用於擴展
            sessionStrategy.onAuthentication(authResult, request, response);
        }
        catch (InternalAuthenticationServiceException failed) {
      //2.一、發生異常,登陸失敗,進入登陸失敗handler回調
            unsuccessfulAuthentication(request, response, failed);
            return;
        }
        catch (AuthenticationException failed) {
      //2.一、發生異常,登陸失敗,進入登陸失敗處理器
            unsuccessfulAuthentication(request, response, failed);
            return;
        }
        // 3.一、登陸成功,進入登陸成功處理器。
        successfulAuthentication(request, response, chain, authResult);
    }

二、successfulAuthentication

登陸成功處理器

protected void successfulAuthentication(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain, Authentication authResult)
            throws IOException, ServletException {
    //一、登陸成功 將認證成功的Authentication對象存入SecurityContextHolder中
    //      SecurityContextHolder本質是一個ThreadLocal
        SecurityContextHolder.getContext().setAuthentication(authResult);
    //二、若是開啓了記住我功能,將調用rememberMeServices的loginSuccess 將生成一個token
      //   將token放入cookie中這樣 下次就不用登陸就能夠認證。具體關於記住我rememberMeServices的相關分析我                    們下面幾篇文章會深刻分析的。
        rememberMeServices.loginSuccess(request, response, authResult);
        // Fire event
    //三、發佈一個登陸事件。
        if (this.eventPublisher != null) {
            eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
                    authResult, this.getClass()));
        }
    //四、調用咱們本身定義的登陸成功處理器,這樣也是咱們擴展得知登陸成功的一個擴展點。
        successHandler.onAuthenticationSuccess(request, response, authResult);
    }

三、unsuccessfulAuthentication

登陸失敗處理器

protected void unsuccessfulAuthentication(HttpServletRequest request,
            HttpServletResponse response, AuthenticationException failed)
            throws IOException, ServletException {
    //一、登陸失敗,將SecurityContextHolder中的信息清空
        SecurityContextHolder.clearContext();
    //二、關於記住我功能的登陸失敗處理
        rememberMeServices.loginFail(request, response);
    //三、調用咱們本身定義的登陸失敗處理器,這裏能夠擴展記錄登陸失敗的日誌。
        failureHandler.onAuthenticationFailure(request, response, failed);
    }

關於AbstractAuthenticationProcessingFilter主要分析就到這。咱們能夠從源碼中知道,當請求進入該過濾器中具體的流程是

  1. 判斷該請求是否要被認證
  2. 調用attemptAuthentication方法開始認證,因爲是抽象方法具體認證邏輯給子類
  3. 若是登陸成功,則將認證結果Authentication對象根據session策略寫入session中,將認證結果寫入到SecurityContextHolder,若是開啓了記住我功能,則根據記住我功能,生成token而且寫入cookie中,最後調用一個successHandler對象的方法,這個對象能夠是咱們配置注入的,用於處理咱們的自定義登陸成功的一些邏輯(好比記錄登陸成功日誌等等)。
  4. 若是登陸失敗,則清空SecurityContextHolder中的信息,而且調用咱們本身注入的failureHandler對象,處理咱們本身的登陸失敗邏輯。

UsernamePasswordAuthenticationFilter

從上面分析咱們能夠知道,UsernamePasswordAuthenticationFilter是繼承於AbstractAuthenticationProcessingFilter,而且實現它的attemptAuthentication方法,來實現認證具體的邏輯實現。接下來,咱們經過閱讀UsernamePasswordAuthenticationFilter的源碼來解讀,它是如何完成認證的。 因爲這裏會涉及UsernamePasswordAuthenticationToken對象構造,因此咱們先看看UsernamePasswordAuthenticationToken的源碼

一、UsernamePasswordAuthenticationToken

// 繼承至AbstractAuthenticationToken 
// AbstractAuthenticationToken主要定義一下在SpringSecurity中toke須要存在一些必須信息
// 例如權限集合  Collection<GrantedAuthority> authorities; 是否定證經過boolean authenticated = false;認證經過的用戶信息Object details;
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
 
  // 未登陸狀況下 存的是用戶名 登陸成功狀況下存的是UserDetails對象
    private final Object principal;
  // 密碼
    private Object credentials;

  /**
  * 構造函數,用戶沒有登陸的狀況下,此時的authenticated是false,表明還沒有認證
  */
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        setAuthenticated(false);
    }

 /**
  * 構造函數,用戶登陸成功的狀況下,多了一個參數 是用戶的權限集合,此時的authenticated是true,表明認證成功
  */
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
            Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true); // must use super, as we override
    }
}

接下來咱們就能夠分析attemptAuthentication方法了。

二、attemptAuthentication

public Authentication attemptAuthentication(HttpServletRequest request,
            HttpServletResponse response) throws AuthenticationException {
     // 一、判斷是否是post請求,若是不是則拋出AuthenticationServiceException異常,注意這裏拋出的異常都在AbstractAuthenticationProcessingFilter#doFilter方法中捕獲,捕獲以後會進入登陸失敗的邏輯。
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
    // 二、從request中拿用戶名跟密碼
        String username = obtainUsername(request);
        String password = obtainPassword(request);
        // 三、非空處理,防止NPE異常
        if (username == null) {
            username = "";
        }
        if (password == null) {
            password = "";
        }
    // 四、除去空格
        username = username.trim();
    // 五、根據username跟password構造出一個UsernamePasswordAuthenticationToken對象 從上文分析可知道,此時的token是未認證的。
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                username, password);
    // 六、配置一下其餘信息 ip 等等
        setDetails(request, authRequest);
   //  七、調用ProviderManger的authenticate的方法進行具體認證邏輯
        return this.getAuthenticationManager().authenticate(authRequest);
    }

ProviderManager

維護一個AuthenticationProvider列表,進行認證邏輯驗證

一、authenticate

public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
    // 一、拿到token的類型。
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        Authentication result = null;
   // 二、遍歷AuthenticationProvider列表
        for (AuthenticationProvider provider : getProviders()) {
      // 三、AuthenticationProvider不支持當前token類型,則直接跳過
            if (!provider.supports(toTest)) {
                continue;
            }

            try {
        // 四、若是Provider支持當前token,則交給Provider完成認證。
                result = provider.authenticate(authentication);
     
            }
            catch (AccountStatusException e) {

                throw e;
            }
            catch (InternalAuthenticationServiceException e) {
                throw e;
            }
            catch (AuthenticationException e) {
                lastException = e;
            }
        }
    // 五、登陸成功 返回登陸成功的token
        if (result != null) {
            eventPublisher.publishAuthenticationSuccess(result);
            return result;
        }

    }

AbstractUserDetailsAuthenticationProvider

一、authenticate

AbstractUserDetailsAuthenticationProvider實現了AuthenticationProvider接口,而且實現了部分方法,DaoAuthenticationProvider繼承於AbstractUserDetailsAuthenticationProvider類,因此咱們先來看看AbstractUserDetailsAuthenticationProvider的實現。

public abstract class AbstractUserDetailsAuthenticationProvider implements
        AuthenticationProvider, InitializingBean, MessageSourceAware {

  // 國際化處理
    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();


    /**
     * 對token一些檢查,具體檢查邏輯交給子類實現,抽象方法
     */
    protected abstract void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException;


  /**
     * 認證邏輯的實現,調用抽象方法retrieveUser根據username獲取UserDetails對象
     */
    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
    
    // 一、獲取usernmae
        String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
                : authentication.getName();

    // 二、嘗試去緩存中獲取UserDetails對象
        UserDetails user = this.userCache.getUserFromCache(username);
    // 三、若是爲空,則表明當前對象沒有緩存。
        if (user == null) {
            cacheWasUsed = false;
            try {
        //四、調用retrieveUser去獲取UserDetail對象,爲何這個方法是抽象方法你們很容易知道,若是UserDetail信息存在關係數據庫 則能夠重寫該方法而且去關係數據庫獲取用戶信息,若是UserDetail信息存在其餘地方,能夠重寫該方法用其餘的方法去獲取用戶信息,這樣絲絕不影響整個認證流程,方便擴展。
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
      catch (UsernameNotFoundException notFound) {
                
                // 捕獲異常 日誌處理 而且往上拋出,登陸失敗。
                if (hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(messages.getMessage(
                            "AbstractUserDetailsAuthenticationProvider.badCredentials",
                            "Bad credentials"));
                }
                else {
                    throw notFound;
                }
            }
        }

        try {
      // 五、前置檢查  判斷當前用戶是否鎖定,禁用等等
            preAuthenticationChecks.check(user);
      // 六、其餘的檢查,在DaoAuthenticationProvider是檢查密碼是否一致
            additionalAuthenticationChecks(user,
                    (UsernamePasswordAuthenticationToken) authentication);
        }
        catch (AuthenticationException exception) {
        
        }

    // 七、後置檢查,判斷密碼是否過時
        postAuthenticationChecks.check(user);

     
        // 八、登陸成功經過UserDetail對象從新構造一個認證經過的Token對象
        return createSuccessAuthentication(principalToReturn, authentication, user);
    }

    
    protected Authentication createSuccessAuthentication(Object principal,
            Authentication authentication, UserDetails user) {
        // 調用第二個構造方法,構造一個認證經過的Token對象
        UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
                principal, authentication.getCredentials(),
                authoritiesMapper.mapAuthorities(user.getAuthorities()));
        result.setDetails(authentication.getDetails());

        return result;
    }

}

接下來咱們具體看看retrieveUser的實現,沒看源碼你們應該也能夠知道,retrieveUser方法應該是調用UserDetailsService去數據庫查詢是否有該用戶,以及用戶的密碼是否一致。

DaoAuthenticationProvider

DaoAuthenticationProvider 主要是經過UserDetailService來獲取UserDetail對象。

一、retrieveUser

protected final UserDetails retrieveUser(String username,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        try {
      // 一、調用UserDetailsService接口的loadUserByUsername方法獲取UserDeail對象
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
       // 二、若是loadedUser爲null 表明當前用戶不存在,拋出異常 登陸失敗。
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException(
                        "UserDetailsService returned null, which is an interface contract violation");
            }
      // 三、返回查詢的結果
            return loadedUser;
        }
    }

二、additionalAuthenticationChecks

protected void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
    // 一、若是密碼爲空,則拋出異常、
        if (authentication.getCredentials() == null) {
            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }

    // 二、獲取用戶輸入的密碼
        String presentedPassword = authentication.getCredentials().toString();

    // 三、調用passwordEncoder的matche方法 判斷密碼是否一致
        if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
            logger.debug("Authentication failed: password does not match stored value");

      // 四、若是不一致 則拋出異常。
            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }
    }

總結

至此,整認證流程已經分析完畢,你們若是有什麼不懂能夠關注個人公衆號一塊兒討論。

學習是一個漫長的過程,學習源碼可能會很困難可是隻要努力必定就會有獲取,你們一致共勉。

程序咖啡廳

相關文章
相關標籤/搜索