Spring Security系列教程11--Spring Security認證授權流程
highlight: a11y-dark
持續創作,加速成長!這是我參與「掘金日新計劃 · 10 月更文挑戰」的第11天,點擊查看活動詳情
前言
在上一章節中,一一哥 帶大家認識了Spring Security內部關於認證授權的幾個核心API,以及這幾個核心API之間的引用關係,掌握了這些之後,我們就能進一步研究分析認證授權的內部實現原理了。這樣才真正的達到了 "知其所以然" !
本篇文章中,壹哥 帶各位小夥伴進一步分析認證授權的源碼實現,請各位再堅持一下吧......
一. Spring Security認證授權流程圖概述
在上一章節中,壹哥就給各位貼出過Spring Security的認證授權流程圖,該圖展示了認證授權時經歷的核心API,並且展示了認證授權流程。接下來我們結合源碼,一點點分析認證和授權的實現過程。
二. 簡要剖析認證授權實現流程的代碼邏輯
Spring Security的認證授權流程其實是非常複雜的,在我們對源碼還不夠了解的情況下,壹哥先給各位簡要概括一下這個認證和授權流程,大致如下:
- 用户登錄前,默認生成的Authentication對象處於未認證狀態,登錄時會交由AuthenticationManager負責進行認證。
- AuthenticationManager會將Authentication中的用户名/密碼與UserDetails中的用户名/密碼對比,完成認證工作,認證成功後會生成一個已認證狀態的Authentication對象;
- 最後把認證通過的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
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中默認執行的過濾器順序如下:
- WebAsyncManagerIntegrationFilter
- SecurityContextPersistenceFilter
- HeaderWriterFilter
- CsrfFilter
- LogoutFilter
- UsernamePasswordAuthenticationFilter
- DefaultLoginPageGeneratingFilter
- DefaultLogoutPageGeneratingFilter
- RequestCacheAwareFilter
- SecurityContextHolderAwareRequestFilter
- AnonymousAuthenticationFilter:如果之前的認證機制都沒有更新 SecurityContextHolder 擁有的 Authentication,那麼一個 AnonymousAuthenticationToken 將會設給 SecurityContextHolder。
- SessionManagementFilter
- ExceptionTranslationFilter:用於處理在 FilterChain 範圍內拋出的 AccessDeniedException 和 AuthenticationException,並把它們轉換為對應的 Http 錯誤碼返回或者跳轉到對應的頁面。
- 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);
這幾個方法具體的作用如下:
- 首先執行 requiresAuthentication(HttpServletRequest, HttpServletResponse) 方法, 來決定是否需要進行驗證操作;
- 如果需要驗證,接着就會調用 attemptAuthentication(HttpServletRequest, HttpServletResponse) 方法來封裝用户信息再進行驗證,可能會有三種結果產生:
- 如果返回的Authentication對象為Null,則表示身份驗證不完整,該方法將立即結束返回。
- 如果返回的Authentication對象不為空,則調用配置的 SessionAuthenticationStrategy 對象,執行onAuthentication()方法,然後調用 successfulAuthentication(HttpServletRequest,HttpServletResponse,FilterChain,Authentication) 方法。
- 驗證時如果發生 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));
}
......其他略......
} ```
我來解釋一下上面的源碼:
- 首先我們從構造方法中可以得知,該過濾器 只對post請求方式的"/login"接口有效;
- 然後在該過濾器中,再利用 obtainUsername 和 obtainPassword 方法,提取出請求裏邊的用户名/密碼,提取方式就是 request.getParameter,這也是為什麼 Spring Security 中默認的表單登錄要通過 key/value 的形式傳遞參數,而不能傳遞 JSON 參數。如果像傳遞 JSON 參數,我們可以通過修改這裏的代碼來進行實現。
- 獲取到請求裏傳遞來的用户名/密碼之後,接下來會 構造一個 UsernamePasswordAuthenticationToken 對象,傳入 username 和 password。 其中 username 對應了 UsernamePasswordAuthenticationToken 中的 principal 屬性,而 password 則對應了它的 credentials 屬性。
- 接下來 再利用 setDetails 方法給 details 屬性賦值,UsernamePasswordAuthenticationToken 本身是沒有 details 屬性的,這個屬性是在它的父類 AbstractAuthenticationToken 中定義的。details 是一個對象,這個對象裏邊存放的是 WebAuthenticationDetails 實例,該實例主要描述了 請求的 remoteAddress 以及請求的 sessionId 這兩個信息。
- 最後一步,就是利用AuthenticationManager對象來調用 authenticate() 方法去做認證校驗。
5. AuthenticationManager與ProviderManager
咱們在上面 UsernamePasswordAuthenticationToken類的 attemptAuthentication() 方法中得知,該方法的最後一步會進行關於認證的校驗,而要進行認證操作首先要獲取到一個 AuthenticationManager 對象,這裏默認拿到的是AuthenticationManager的子類ProviderManager ,如下圖所示:
所以接下來我們要進入到 ProviderManager 的 authenticate()方法中,來看看認證到底是怎麼實現的。因為這個方法的實現代碼比較長,我這裏僅摘列出幾個重要的地方:
ProviderManager類中的authenticate()方法代碼很長,我通過截圖,把該方法中的重點地方做了紅色標記,方便大家重點記憶。
其實Spring Security中關於認證的重要邏輯幾乎都是在這裏完成的,所以接下來我分步驟,一點點帶大家分析該認證的實現流程。
- 首先利用反射,獲取到要認證的 authentication 對象的 Class字節碼,如下圖:
- 判斷當前 provider 是否支持該 authentication 對象,如下圖:
如果當前provider不支持該 authentication 對象,則退出當前判斷,進行下一次判斷。
- 如果支持,則調用 provider 的 authenticate 方法開始做校驗,校驗完成後,會返回一個新的 Authentication,如下圖:
- 這裏的 provider 會有多個,我們在上一章節給大家介紹過,如下圖:
這裏如果 provider 的 authenticate 方法沒能返回一個 Authentication 認證對象,則會調用 provider 的 parent 對象中的 authenticate 方法繼續校驗。
- 而如果通過了校驗,返回了一個Authentication 認證對象,則調用 copyDetails()方法把舊 Token 的 details 屬性拷貝到新的 Token 中,如下圖。
- 接下來會調用 eraseCredentials()方法來擦除憑證信息,也就是我們的密碼,這個擦除方法比較簡單,就是將 Token 中的 credentials 屬性置空。
- 最後通過 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);
}
```
結合上面的源碼,我們再做進一步的分析梳理:
- 我在上面説過,DaoAuthenticationProvider這個子類並沒有重寫authenticate()方法,而是在父類AbstractUserDetailsAuthenticationProvider中實現的 。 AbstractUserDetailsAuthenticationProvider類中的authenticate()方法執行時, 首先會從 Authentication 提取出登錄用户名,如下圖所示:
- 然後利用得到的 username,先去緩存中查詢是否有該用户,如下所示:
- 如果緩存中沒有該用户,則去執行 retrieveUser() 方法獲取當前用户對象。 而這個retrieveUser()方法是個抽象方法,在AbstractUserDetailsAuthenticationProvider類中並沒有實現,是由子類DaoAuthenticationProvider來實現的。
- 在DaoAuthenticationProvider類的retrieveUser() 方法中, 會調用getUserDetailsService()方法,得到 UserDetailsService對象,執行 我們自己在登錄時候編寫的 loadUserByUsername()方法 ,然後返回一個UserDetails對象,也就是我們的登錄對象。 如下圖所示。
- 接下來會繼續往下執行preAuthenticationChecks.check()方法,檢驗 user 中各賬户屬性是否正常,例如賬户是否被禁用、是否被鎖定、是否過期等,如下所示。
- 接着會繼續往下執行additionalAuthenticationChecks()方法,進行密碼比對。而該方法也是抽象方法,也是由子類DaoAuthenticationProvider進行實現。我們在註冊用户時對密碼加密之後,Spring Security就是在這裏進行密碼比對的 。 如下所示。
- 然後在 postAuthenticationChecks.check()方法中檢查密碼是否過期,如下所示。
- 然後判斷是否進行了緩存,如果未進行緩存,則執行緩存操作,這個緩存是由 SpringCacheBasedUserCache類來實現的。
我們這裏如果沒有對緩存做配置,則會執行默認的緩存配置操作。如果我們對緩存進行了自定義的配置,比如配置了RedisCache,就可以把對象緩存到redis中。
- 接下來有一個 forcePrincipalAsString 屬性,該屬性表示 是否強制將 Authentication 中的 principal 屬性設置為字符串,這個屬性其實我們一開始就在 UsernamePasswordAuthenticationFilter 類中定義為了字符串(即username)。但是默認情況下,當用户登錄成功之後,這個屬性的值就變成當前用户這個對象了。之所以會這樣,就是因為 forcePrincipalAsString 默認為 false,不過這塊其實不用改,就用 false,這樣在後期獲取當前用户信息的時候反而方便很多。
- 最後通過createSuccessAuthentication()方法構建出一個新的 UsernamePasswordAuthenticationToken對象。
- 這樣我們最終得到了認證通過的Authentication對象,並把該對象利用publishAuthenticationSuccess()方法,將該事件發佈出去。
- 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進行認證授權的源碼執行流程,大致就是上面我婆媳剖析的那麼,接下來我對認證過程做個簡單總結:
- 首先用户在登錄表單中,填寫用户名和密碼,進行登錄操作;
- AbstractAuthenticationProcessingFilter結合UsernamePasswordAuthenticationToken過濾器,將獲取到的用户名和密碼封裝成一個實現了 Authentication 接口的實現子類 UsernamePasswordAuthenticationToken對象。
- 將上述產生的 token 對象傳遞給 AuthenticationManager的具體子類ProviderManager 進行登錄認證。
- ProviderManager 認證成功後將會返回一個封裝了用户權限等信息的 Authentication 對象。
- 通過調用 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唄!