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唄!