Spring Security系列教程11--Spring Security認證授權流程

語言: CN / TW / HK

highlight: a11y-dark

持續創作,加速成長!這是我參與「掘金日新計劃 · 10 月更文挑戰」的第11天,點選檢視活動詳情

前言

在上一章節中,一一哥 帶大家認識了Spring Security內部關於認證授權的幾個核心API,以及這幾個核心API之間的引用關係,掌握了這些之後,我們就能進一步研究分析認證授權的內部實現原理了。這樣才真正的達到了 "知其所以然"

本篇文章中,壹哥 帶各位小夥伴進一步分析認證授權的原始碼實現,請各位再堅持一下吧......

一. Spring Security認證授權流程圖概述

在上一章節中,壹哥就給各位貼出過Spring Security的認證授權流程圖,該圖展示了認證授權時經歷的核心API,並且展示了認證授權流程。接下來我們結合原始碼,一點點分析認證和授權的實現過程。

二. 簡要剖析認證授權實現流程的程式碼邏輯

Spring Security的認證授權流程其實是非常複雜的,在我們對原始碼還不夠了解的情況下,壹哥先給各位簡要概括一下這個認證和授權流程,大致如下:

  1. 使用者登入前,預設生成的Authentication物件處於未認證狀態,登入時會交由AuthenticationManager負責進行認證。
  2. AuthenticationManager會將Authentication中的使用者名稱/密碼與UserDetails中的使用者名稱/密碼對比,完成認證工作,認證成功後會生成一個已認證狀態的Authentication物件;
  3. 最後把認證通過的Authentication物件寫入到SecurityContext中,在使用者有後續請求時,可從Authentication中檢查許可權。

我們可以借鑑一下Spring Security官方文件中提供的一個最簡化的認證授權流程程式碼,來認識一下認證授權的實現過程,該程式碼省略了UserDetails操作,只做了簡單認證,可以對認證授權有個大概瞭解。

``` public class AuthenticationExample {

private static AuthenticationManager am = new SampleAuthenticationManager();

public static void main(String[] args) throws Exception {
    BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

    while (true) {
        //模擬輸入使用者名稱密碼
        System.out.println("Please enter your username:");
        String name = in.readLine();

        System.out.println("Please enter your password:");
        String password = in.readLine();

        try {
            //根據使用者名稱/密碼,生成未認證Authentication
            Authentication request = new UsernamePasswordAuthenticationToken(name, password);
            //交給AuthenticationManager 認證
            Authentication result = am.authenticate(request);
            //將已認證的Authentication放入SecurityContext
            SecurityContextHolder.getContext().setAuthentication(result);
            break;
        } catch (AuthenticationException e) {
            System.out.println("Authentication failed: " + e.getMessage());
        }
    }

    System.out.println("Successfully authenticated. Security context contains: "
            + SecurityContextHolder.getContext().getAuthentication());
}

}

//認證類 class SampleAuthenticationManager implements AuthenticationManager { //配置一個簡單的使用者許可權集合 static final List AUTHORITIES = new ArrayList();

static {
    AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
}

public Authentication authenticate(Authentication auth) throws AuthenticationException {
    //如果使用者名稱和密碼一致,則登入成功,這裡只做了簡單認證
    if (auth.getName().equals(auth.getCredentials())) {
        //認證成功,生成已認證Authentication,比未認證多了許可權
        return new UsernamePasswordAuthenticationToken(auth.getName(), auth.getCredentials(), AUTHORITIES);
    }

    throw new BadCredentialsException("Bad Credentials");
}

} ```

以上程式碼只是簡單的模擬了認證的過程,那麼真正的認證授權操作,是不是也這樣呢?接下來請跟著 一一哥,咱們結合原始碼進行詳細的剖析。

三. Spring Security認證流程原始碼詳解

1. Spring Security過濾器鏈執行順序

在 Spring Security 中,與認證、授權相關的校驗其實都是利用一系列的過濾器來完成的,這些過濾器共同組成了一個過濾器鏈,如下圖所示:

你可能會問,我們怎麼知道這些過濾器在執行?其實我們只要開啟Spring Security的debug除錯模式,開發時就可以在控制檯看到這些過濾器的執行順序,如下:

所以在上圖中可以看到,Spring Security中預設執行的過濾器順序如下:

  1. WebAsyncManagerIntegrationFilter
  2. SecurityContextPersistenceFilter
  3. HeaderWriterFilter
  4. CsrfFilter
  5. LogoutFilter
  6. UsernamePasswordAuthenticationFilter
  7. DefaultLoginPageGeneratingFilter
  8. DefaultLogoutPageGeneratingFilter
  9. RequestCacheAwareFilter
  10. SecurityContextHolderAwareRequestFilter
  11. AnonymousAuthenticationFilter:如果之前的認證機制都沒有更新 SecurityContextHolder 擁有的 Authentication,那麼一個 AnonymousAuthenticationToken 將會設給 SecurityContextHolder。
  12. SessionManagementFilter
  13. ExceptionTranslationFilter:用於處理在 FilterChain 範圍內丟擲的 AccessDeniedException 和 AuthenticationException,並把它們轉換為對應的 Http 錯誤碼返回或者跳轉到對應的頁面。
  14. FilterSecurityInterceptor:負責保護 Web URI,並且在訪問被拒絕時丟擲異常。

2. SecurityContextPersistenceFilter

在上面的過濾器鏈中,我們可以看到SecurityContextPersistenceFilter這個過濾器。SecurityContextPersistenceFilter是Security中的一個攔截器,它的執行時機非常早,當請求來臨時它會從SecurityContextRepository中把SecurityContext物件取出來,然後放入SecurityContextHolder的ThreadLocal中。在所有攔截器都處理完成後,再把SecurityContext存入SecurityContextRepository,並清除SecurityContextHolder內的SecurityContext引用

3. AbstractAuthenticationProcessingFilter

在上面圖中所展示的一系列的過濾器中,和認證授權直接相關的過濾器是 AbstractAuthenticationProcessingFilter 和 UsernamePasswordAuthenticationFilter

但是你可能又會問,怎麼沒看到 AbstractAuthenticationProcessingFilter 這個過濾器呢?這是因為它是一個抽象的父類,其內部定義了認證處理的過程,UsernamePasswordAuthenticationFilter 就繼承自 AbstractAuthenticationProcessingFilter。 如下圖所示:

從上圖中我們可以看出,AbstractAuthenticationProcessingFilter的父類是GenericFilterBean,而 GenericFilterBean 是 Spring 框架中的過濾器類,最終的父介面是Filter, 也就是我們熟悉的過濾器。那麼既然是Filter的子類,肯定會執行其中最核心的doFilter()方法,我們來看看AbstractAuthenticationProcessingFilter的doFilter()方法原始碼。

從上面的原始碼中我們可以看出,doFilter()方法的內部實現並不複雜,裡面就只引用了5個方法,如下:

  • requiresAuthentication(request, response);
  • authResult = attemptAuthentication(request, response);
  • sessionStrategy.onAuthentication(authResult, request, response);
  • unsuccessfulAuthentication(request, response, failed);
  • successfulAuthentication(request, response, chain, authResult);

這幾個方法具體的作用如下:

  1. 首先執行 requiresAuthentication(HttpServletRequest, HttpServletResponse) 方法, 來決定是否需要進行驗證操作;
  2. 如果需要驗證,接著就會呼叫 attemptAuthentication(HttpServletRequest, HttpServletResponse) 方法來封裝使用者資訊再進行驗證,可能會有三種結果產生:
    1. 如果返回的Authentication物件為Null,則表示身份驗證不完整,該方法將立即結束返回。
    2. 如果返回的Authentication物件不為空,則呼叫配置的 SessionAuthenticationStrategy 物件,執行onAuthentication()方法,然後呼叫 successfulAuthentication(HttpServletRequest,HttpServletResponse,FilterChain,Authentication) 方法。
    3. 驗證時如果發生 AuthenticationException,則執行unsuccessfulAuthentication(HttpServletRequest, HttpServletResponse, AuthenticationException) 方法。

我們在上面的原始碼中得知有個attemptAuthentication()方法,該方法是一個抽象方法,由子類來具體實現。

這個抽象方法由子類UsernamePasswordAuthenticationFilter來實現,如下圖。

AbstractAuthenticationProcessingFilter是一個比較複雜的類,內部的處理流程比較多,我們做個簡單梳理,如下圖所示:

綜上所述,我們可知AbstractAuthenticationProcessingFilter類,可以負責處理所有的HTTP Request和Response物件,並將其封裝成AuthenticationMananger可以處理的Authentication物件。在身份驗證成功或失敗之後,將對應的行為轉換為HTTP的Response物件。同時還能處理一些Web特有的資源,比如Session和Cookie等操作。

到此為止,我們已經把AbstractAuthenticationProcessingFilter這個類的核心功能給瞭解清楚了,接下來我們學習AbstractAuthenticationProcessingFilter的子類UsernamePasswordAuthenticationFilter。

4. UsernamePasswordAuthenticationFilter

我在上一小節中說過,在 Spring Security 中,認證與授權的相關校驗是在AbstractAuthenticationProcessingFilter這個過濾器中完成的,但是該類是一個抽象類,它有個子類UsernamePasswordAuthenticationFilter,該類是和認證直接相關的過濾器實現子類。我們來看一下該類的核心原始碼:

``` public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

...
...

// ~ Constructors
// ===================================================================================================

public UsernamePasswordAuthenticationFilter() {
    super(new AntPathRequestMatcher("/login", "POST"));
}

public Authentication attemptAuthentication(HttpServletRequest request,
        HttpServletResponse response) throws AuthenticationException {
    if (postOnly && !request.getMethod().equals("POST")) {
        throw new AuthenticationServiceException(
                "Authentication method not supported: " + request.getMethod());
    }

    String username = obtainUsername(request);
    String password = obtainPassword(request);

    if (username == null) {
        username = "";
    }

    if (password == null) {
        password = "";
    }

    username = username.trim();

    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
            username, password);

    // Allow subclasses to set the "details" property
    setDetails(request, authRequest);

    return this.getAuthenticationManager().authenticate(authRequest);
}

@Nullable
protected String obtainPassword(HttpServletRequest request) {
    return request.getParameter(passwordParameter);
}

@Nullable
protected String obtainUsername(HttpServletRequest request) {
    return request.getParameter(usernameParameter);
}

protected void setDetails(HttpServletRequest request,
        UsernamePasswordAuthenticationToken authRequest) {
    authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}

......其他略......

} ```

我來解釋一下上面的原始碼:

  1. 首先我們從構造方法中可以得知,該過濾器 只對post請求方式的"/login"介面有效
  2. 然後在該過濾器中,再利用 obtainUsername 和 obtainPassword 方法,提取出請求裡邊的使用者名稱/密碼,提取方式就是 request.getParameter,這也是為什麼 Spring Security 中預設的表單登入要通過 key/value 的形式傳遞引數,而不能傳遞 JSON 引數。如果像傳遞 JSON 引數,我們可以通過修改這裡的程式碼來進行實現。
  3. 獲取到請求裡傳遞來的使用者名稱/密碼之後,接下來會 構造一個 UsernamePasswordAuthenticationToken 物件傳入 username 和 password。 其中 username 對應了 UsernamePasswordAuthenticationToken 中的 principal 屬性,而 password 則對應了它的 credentials 屬性。
  4. 接下來 再利用 setDetails 方法給 details 屬性賦值,UsernamePasswordAuthenticationToken 本身是沒有 details 屬性的,這個屬性是在它的父類 AbstractAuthenticationToken 中定義的。details 是一個物件,這個物件裡邊存放的是 WebAuthenticationDetails 例項,該例項主要描述了 請求的 remoteAddress 以及請求的 sessionId 這兩個資訊。
  5. 最後一步,就是利用AuthenticationManager物件來呼叫 authenticate() 方法去做認證校驗

5. AuthenticationManager與ProviderManager

咱們在上面 UsernamePasswordAuthenticationToken類的 attemptAuthentication() 方法中得知,該方法的最後一步會進行關於認證的校驗,而要進行認證操作首先要獲取到一個 AuthenticationManager 物件,這裡預設拿到的是AuthenticationManager的子類ProviderManager ,如下圖所示:

所以接下來我們要進入到 ProviderManager 的 authenticate()方法中,來看看認證到底是怎麼實現的。因為這個方法的實現程式碼比較長,我這裡僅摘列出幾個重要的地方:

ProviderManager類中的authenticate()方法程式碼很長,我通過截圖,把該方法中的重點地方做了紅色標記,方便大家重點記憶。

其實Spring Security中關於認證的重要邏輯幾乎都是在這裡完成的,所以接下來我分步驟,一點點帶大家分析該認證的實現流程。

  1. 首先利用反射,獲取到要認證的 authentication 物件的 Class位元組碼,如下圖:

  1. 判斷當前 provider 是否支援該 authentication 物件,如下圖:

如果當前provider不支援該 authentication 物件,則退出當前判斷,進行下一次判斷。

  1. 如果支援,則呼叫 provider 的 authenticate 方法開始做校驗,校驗完成後,會返回一個新的 Authentication,如下圖:

  1. 這裡的 provider 會有多個,我們在上一章節給大家介紹過,如下圖:

這裡如果 provider 的 authenticate 方法沒能返回一個 Authentication 認證物件,則會呼叫 provider 的 parent 物件中的 authenticate 方法繼續校驗。

  1. 而如果通過了校驗,返回了一個Authentication 認證物件,則呼叫 copyDetails()方法把舊 Token 的 details 屬性拷貝到新的 Token 中,如下圖。

  1. 接下來會呼叫 eraseCredentials()方法來擦除憑證資訊,也就是我們的密碼,這個擦除方法比較簡單,就是將 Token 中的 credentials 屬性置空。

  1. 最後通過 publishAuthenticationSuccess() 方法將認證成功的事件廣播出去。

在以上程式碼的for迴圈中,第一次拿到的 provider 是一個 AnonymousAuthenticationProvider。這個provider 是不支援 UsernamePasswordAuthenticationToken的,所以會直接在 provider.supports()方法中返回 false,結束當前for迴圈,並進入到下一個 if 判斷中,最後直接呼叫 parent 的 authenticate 方法進行校驗。

而parent就是 ProviderManager物件,所以會再次回到這個authenticate()方法中 當再次回到authenticate() 方法時,provider會變成第二個Provider,即DaoAuthenticationProvider 這個 provider 是支援 UsernamePasswordAuthenticationToken 的,所以會順利進入到該類的 authenticate()方法去執行。

6. DaoAuthenticationProvider

DaoAuthenticationProvider繼承自AbstractUserDetailsAuthenticationProvider, DaoAuthenticationProvider類結構如下所示:

DaoAuthenticationProvider類中並沒有重寫 authenticate() 方法authenticate() 方法是在父類AbstractUserDetailsAuthenticationProvider中實現的 所以我們看看AbstractUserDetailsAuthenticationProvider#authenticate()方法的原始碼,這裡我對該原始碼做了一些簡化:

``` public Authentication authenticate(Authentication authentication) throws AuthenticationException { ......

    //獲取authentication中儲存的使用者名稱
    String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
            : authentication.getName();

    //判斷是否使用了快取
    boolean cacheWasUsed = true;
    UserDetails user = this.userCache.getUserFromCache(username);

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

        try {
            //retrieveUser()是一個抽象方法,由子類DaoAuthenticationProvider來實現,用於根據使用者名稱查詢使用者。
            user = retrieveUser(username,
                    (UsernamePasswordAuthenticationToken) authentication);
        }
        catch (UsernameNotFoundException notFound) {
            ......
    }

    try {
        //進行必要的認證前和額外認證的檢查
        preAuthenticationChecks.check(user);

        //這是抽象方法,由子類DaoAuthenticationProvider來實現,用於進行密碼對比
        additionalAuthenticationChecks(user,
                (UsernamePasswordAuthenticationToken) authentication);
    }
    catch (AuthenticationException exception) {
        if (cacheWasUsed) {
            //在發生異常時,嘗試著從快取中進行物件的載入
            cacheWasUsed = false;
            user = retrieveUser(username,
                    (UsernamePasswordAuthenticationToken) authentication);
            preAuthenticationChecks.check(user);
            additionalAuthenticationChecks(user,
                    (UsernamePasswordAuthenticationToken) authentication);
        }
        else {
            throw exception;
        }
    }

    //認證後的檢查操作
    postAuthenticationChecks.check(user);

    if (!cacheWasUsed) {
        this.userCache.putUserInCache(user);
    }

    Object principalToReturn = user;

    if (forcePrincipalAsString) {
        principalToReturn = user.getUsername();
    }

    //認證成功後,封裝認證物件
    return createSuccessAuthentication(principalToReturn, authentication, user);
}

```

結合上面的原始碼,我們再做進一步的分析梳理:

  1. 我在上面說過,DaoAuthenticationProvider這個子類並沒有重寫authenticate()方法,而是在父類AbstractUserDetailsAuthenticationProvider中實現的 AbstractUserDetailsAuthenticationProvider類中的authenticate()方法執行時, 首先會從 Authentication 提取出登入使用者名稱,如下圖所示:

  1. 然後利用得到的 username,先去快取中查詢是否有該使用者,如下所示:

  1. 如果快取中沒有該使用者,則去執行 retrieveUser() 方法獲取當前使用者物件。 而這個retrieveUser()方法是個抽象方法,在AbstractUserDetailsAuthenticationProvider類中並沒有實現,是由子類DaoAuthenticationProvider來實現的。

  1. DaoAuthenticationProvider類的retrieveUser() 方法中, 會呼叫getUserDetailsService()方法,得到 UserDetailsService物件,執行 我們自己在登入時候編寫的 loadUserByUsername()方法 ,然後返回一個UserDetails物件,也就是我們的登入物件。 如下圖所示。

  1. 接下來會繼續往下執行preAuthenticationChecks.check()方法,檢驗 user 中各賬戶屬性是否正常,例如賬戶是否被禁用、是否被鎖定、是否過期等,如下所示。

  1. 接著會繼續往下執行additionalAuthenticationChecks()方法,進行密碼比對。而該方法也是抽象方法,也是由子類DaoAuthenticationProvider進行實現。我們在註冊使用者時對密碼加密之後,Spring Security就是在這裡進行密碼比對的 如下所示。

  1. 然後在 postAuthenticationChecks.check()方法中檢查密碼是否過期,如下所示。

  1. 然後判斷是否進行了快取,如果未進行快取,則執行快取操作,這個快取是由 SpringCacheBasedUserCache類來實現的。

我們這裡如果沒有對快取做配置,則會執行預設的快取配置操作。如果我們對快取進行了自定義的配置,比如配置了RedisCache,就可以把物件快取到redis中。

  1. 接下來有一個 forcePrincipalAsString 屬性,該屬性表示 是否強制將 Authentication 中的 principal 屬性設定為字串,這個屬性其實我們一開始就在 UsernamePasswordAuthenticationFilter 類中定義為了字串(即username)。但是預設情況下,當用戶登入成功之後,這個屬性的值就變成當前使用者這個物件了。之所以會這樣,就是因為 forcePrincipalAsString 預設為 false,不過這塊其實不用改,就用 false,這樣在後期獲取當前使用者資訊的時候反而方便很多。

  1. 最後通過createSuccessAuthentication()方法構建出一個新的 UsernamePasswordAuthenticationToken物件

  1. 這樣我們最終得到了認證通過的Authentication物件,並把該物件利用publishAuthenticationSuccess()方法,將該事件釋出出去。

  1. Spring Security會監聽這個事件,接收到這個Authentication物件,進而呼叫 SecurityContextHolder.getContext().setAuthentication(...)方法,將 AuthenticationManager返回的 Authentication物件,儲存在當前的 SecurityContext 物件中。

7. 儲存Authentication認證資訊

我們在上面說,Authentication認證資訊最終是儲存在SecurityContext 物件中的,但是具體的程式碼是在哪裡實現的呢?

我們來到 UsernamePasswordAuthenticationFilter 的父類 AbstractAuthenticationProcessingFilter 中,這個類我們經常會見到,因為很多時候當我們想要在 Spring Security 自定義一個登入驗證碼或者將登入引數改為 JSON 的時候,我們都需自定義過濾器繼承自 AbstractAuthenticationProcessingFilter。

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

    HttpServletRequest request = (HttpServletRequest) req;
    HttpServletResponse response = (HttpServletResponse) res;

    if (!requiresAuthentication(request, response)) {
        chain.doFilter(request, response);

        return;
    }

    ......

    Authentication authResult;

    try {
        authResult = attemptAuthentication(request, response);
        if (authResult == null) {
            // return immediately as subclass has indicated that it hasn't completed
            // authentication
            return;
        }
        sessionStrategy.onAuthentication(authResult, request, response);
    }
    catch (InternalAuthenticationServiceException failed) {
        logger.error(
                "An internal error occurred while trying to authenticate the user.",
                failed);
        unsuccessfulAuthentication(request, response, failed);

        return;
    }
    catch (AuthenticationException failed) {
        // Authentication failed
        unsuccessfulAuthentication(request, response, failed);

        return;
    }

    // Authentication success
    if (continueChainBeforeSuccessfulAuthentication) {
        chain.doFilter(request, response);
    }

    //處理認證後的操作
    successfulAuthentication(request, response, chain, authResult);
}

```

在上面的原始碼中,有個successfulAuthentication()方法,如下圖。

當UsernamePasswordAuthenticationFilter#attemptAuthentication()方法被觸發執行時,如果登入時丟擲異常,unsuccessfulAuthentication()方法會被呼叫;而當登入成功時 successfulAuthentication()方法則會被呼叫,正是這個方法來儲存的Authentication認證資訊,我們來看看這個原始碼。

在這裡有一段很重要的程式碼,就是 SecurityContextHolder.getContext().setAuthentication(authResult),登入成功的使用者資訊就被儲存在這裡。 在認證成功後,我們就可以在任何地方,通過 SecurityContextHolder.getContext()獲取到Authentication認證資訊。

最後大家還看到還有一個 successHandler.onAuthenticationSuccess()方法,這是我們在 SecurityConfig中配置的登入成功時的回撥處理方法,就是在這裡被觸發。

8. ExceptionTranslationFilter

Spring Security在進行認證授權的過程中,可能會產生各種認證授權異常。對於這些異常,都是由ExceptionTranslationFilter來捕獲過濾器鏈中產生的所有異常並進行處理的但是它只會處理兩類異常:AuthenticationException 和 AccessDeniedException,其它的異常它會繼續丟擲。

如果捕獲到的是 AuthenticationException異常,那麼將會使用其對應的 AuthenticationEntryPoint 裡的commence()方法來處理。 在處理之前,ExceptionTranslationFilter先使用 RequestCache 將當前的HttpServerletRequest的資訊儲存起來,以至於使用者認證登入成功後可以跳轉到之前指定跳轉到的介面。

如果捕獲到的是 AccessDeniedException異常,那麼將根據當前使用者是否已經登入認證做出不同的處理。如果未登入,則會使用關聯的 AuthenticationEntryPoint 的 commence()方法來進行處理;否則將會使用關聯的 AccessDeniedHandler 的handle()方法來進行處理。

9. FilterSecurityInterceptor

當我們經歷了前面一系列的認證授權處理後,最後還有一個FilterSecurityInterceptor 用於保護Http資源,它內部引用了一個AccessDecisionManager和一個AuthenticationManager物件。它會從 SecurityContextHolder 獲取 Authentication物件,然後通過 SecurityMetadataSource 可以得知當前是否在請求受保護的資源。如果請求的是那些受保護的資源,如果Authentication.isAuthenticated() 返回false或者FilterSecurityInterceptor的alwaysReauthenticate 屬性為 true,那麼將會使用其引用的 AuthenticationManager 再認證一次。 認證之後再使用認證後的 Authentication 替換 SecurityContextHolder 中擁有的舊的那個Authentication物件,然後就是利用 AccessDecisionManager 進行許可權的檢查。

注:

  • AuthenticationEntryPoint 是在使用者未登入時,用於引導使用者進行登入認證的;
  • AccessDeniedHandler 是在使用者已經登入後,但是訪問了自身沒有許可權的資源時做出的對應處理。

10. 認證流程總結

Spring Security進行認證授權的原始碼執行流程,大致就是上面我婆媳剖析的那麼,接下來我對認證過程做個簡單總結:

  1. 首先使用者在登入表單中,填寫使用者名稱和密碼,進行登入操作;
  2. AbstractAuthenticationProcessingFilter結合UsernamePasswordAuthenticationToken過濾器,將獲取到的使用者名稱和密碼封裝成一個實現了 Authentication 介面的實現子類 UsernamePasswordAuthenticationToken物件。
  3. 將上述產生的 token 物件傳遞給 AuthenticationManager的具體子類ProviderManager 進行登入認證。
  4. ProviderManager 認證成功後將會返回一個封裝了使用者許可權等資訊的 Authentication 物件。
  5. 通過呼叫 SecurityContextHolder.getContext().setAuthentication(...) 將 AuthenticationManager 返回的 Authentication 物件賦予給當前的 SecurityContext。

我們可以結合下面兩圖,和上面的原始碼,深入理解掌握Spring Security的認證授權流程。

以後出去面試時,給面試官講解這個簡化流程就差不多了。

四. 相關面試題

另外可能我們有這麼一個疑問: 如何 在 request 之間共享 SecurityContext?

既然 SecurityContext 是存放在 ThreadLocal 中的,而且在每次許可權鑑定的時候,都是從 ThreadLocal 中獲取 SecurityContext 中儲存的 Authentication。那麼既然不同的 request 屬於不同的執行緒,為什麼每次都可以從 ThreadLocal 中獲取到當前使用者對應的 SecurityContext 呢?

  • 在 Web 應用中這是通過 SecurityContextPersistentFilter 實現的,預設情況下其在每次請求開始的時候,都會從 session 中獲取 SecurityContext,然後把它設定給 SecurityContextHolder。
  • 在請求結束後又會將 SecurityContextHolder 所持有的 SecurityContext 儲存在 session 中,並且清除 SecurityContextHolder 所持有的 SecurityContext。
  • 這樣當我們第一次訪問系統的時候,SecurityContextHolder 所持有的 SecurityContext 肯定是空的。待我們登入成功後,SecurityContextHolder 所持有的 SecurityContext 就不是空的了,且包含有認證成功的 Authentication 物件。
  • 待請求結束後我們就會將 SecurityContext 存在 session 中,等到下次請求的時候就可以從 session 中獲取到該 SecurityContext 並把它賦予給 SecurityContextHolder 了。
  • 由於 SecurityContextHolder 已經持有認證過的 Authentication 物件了,所以下次訪問的時候也就不再需要進行登入認證了。

到此為止,我就給大家剖析了Spring Security中最核心的認證授權原始碼,也就是底層執行原理,如果你可以把這些原始碼理解掌握了,出去面試時,就靠這一個知識點,就足以征服面試官,讓面試官臣服在你的“牛逼”之下。

你學會了嗎?評論區留言666唄!