2021-03-12
要實作 spring security 裡面認証(authentication) & 援權(authorization) 功能時,大多數文件只會說要實作 UserDetailsService、UserDetails、WebSecurityConfigurerAdapter 這儿個 class 或 inteface 就可以搞定一切。但這儿個 class 間到底怎麼交互作用通常都不提,讓人覺得實作參考的範例程式碼很簡單易懂、但背後原理卻不知怎麼回事。程式這樣改一改就可以跑讓人覺得很像黑魔法,哪天要微調時很可能因為不知背後原理就改不動了。
因此我決定把背後原理搞清楚並整理出重點摘要。既然是摘要只會挑關鍵重點,建議搭配 spring security 裡的 source code 一起看,比較好理解內容。
程式碼依照下列順序進行認証動作
封裝了用戶權限等資料
的 Authentication 實例。而且這個實例內容裡 不帶用戶憑証或密碼
。上述流程項次 1,2,3,5 實作程式碼可參考 BasicAuthenticationFilter.doFilterInternal(...)
method,source code 與關鍵說明如下
@Override
protected void doFilterInternal(HttpServletRequest request,HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
...略
try {
String[] tokens = extractAndDecodeHeader(header, request);
String username = tokens[0];
if (authenticationIsRequired(username)) {
// step1. 將用戶輸入的帳號密碼轉換成 UsernamePasswordAuthenticationToken DTO
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, tokens[1]);
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
// step2. 利用 AuthenticationManager.authenticate(...) method 進行用戶認証
// 認証失敗時丟出 Exception,認証成功時將用戶資料(指 UserDetails)、權限存
// 成一個新的 Authentication DTO
//
// AuthenticationManager.authenticate(...) method 裡的 "尋找用戶資料" 程式片段
// 通常是呼叫 UserDetailsService.loadUserByUsername(...) method
Authentication authResult = this.authenticationManager.authenticate(authRequest);
// step3. SecurityContextHolder 存放授權成功的 Authentication DTO
SecurityContextHolder.getContext().setAuthentication(authResult);
}
}
catch (AuthenticationException failed) {
// 授權失敗時,要把 SecurityContextHolder 裡的 Authentication DTO 清除
SecurityContextHolder.clearContext();
}
...略
}
上述流程項次 4 實作程式碼可參考 AbstractUserDetailsAuthenticationProvider.authenticate(...)
method,source code 與關鍵說明如下
public Authentication authenticate(Authentication authentication)throws AuthenticationException {
...略
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED": authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
// step1. retrieveUser(...) method 呼叫 UserDetailsService.loadUserByUsername(...) method
// 查出用戶資料(指 UserDetails)
user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
...略
throw notFound;
}
}
try {
...略
// step2. 用戶資料(指 UserDetails) 跟 UsernamePasswordAuthenticationToken 內容比對
// 進行用戶認証,認証失敗時丟出 Exception。
additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
...略
throw exception;
}
...略
// step3. 認証成功時將用戶資料(指 UserDetails)、權限存成一個新的 Authentication DTO
return createSuccessAuthentication(principalToReturn, authentication, user);
}
Authentication
Authentication interface 用來存放用戶認證資料,在用戶登錄認證之前相關資料會封裝為一個 Authentication instance,在登錄認證成功之後又會生成一個資料更全面,包含用戶權限等資料的 Authentication instance,然後把它保存在 SecurityContextHolder 所持有的 SecurityContext 中,供後續的程序進行呼叫,如訪問權限的鑑定等。
Authentication.getPrincipal() : 認証成功前存放 "用戶登入id",認証成功後存放 "用戶對應的 UserDetails"
Authentication.getCredentials() : 認証成功前存放 "用戶登入密碼",認証成功後通常不帶任何資料
Authentication.getAuthorities() : 認証成功前不帶任何資料,認証成功後存放 "用戶權限"
AuthenticationManager 和 AuthenticationProvider
AuthenticationManager 是一個用來處理認證(Authentication)請求的 interface,裡面只定義了 authenticate() 方法,該方法只接收一個代表認證請求的 Authentication 實例作為參數,如果認證成功,則會返回一個封裝了當前用戶權限等資料的 Authentication 實例進行返回。
AuthenticationManager 的預設實現是 ProviderManager。認證是由 AuthenticationManager 來管理的,但是真正進行認證的是 AuthenticationManager 中定義的 AuthenticationProvider。AuthenticationManager 中可以定義有多個 AuthenticationProvider。如果沒有指定對應關聯的 AuthenticationProvider 實例,Spring Security 預設會使用 DaoAuthenticationProvider。DaoAuthenticationProvider 在進行認證的時候需要一個 UserDetailsService 來獲取用戶的資料 UserDetails。
檢核認證請求最常用的方法是根據請求的用戶名產生對應的 UserDetails,然後比對 UserDetails 的密碼與認證請求的密碼是否一致,一致則表示認證通過。Spring Security 內部的 DaoAuthenticationProvider 就是使用的這種方式。其內部使用 UserDetailsService 來負責產生 UserDetails。在認證成功以後會產生包含用戶權限等資料的的 UserDetails 實例,並將它封裝在返回的認證成功 Authentication 實例。認證成功返回的 Authentication 實例將會保存在當前的 SecurityContext 中。預設情況下,在認證成功後 ProviderManager 將清除返回的 Authentication 中的憑證資料,如密碼。
UserDetailsService
通過 Authentication.getPrincipal() 的返回類型是 Object,但很多情況下其返回的其實是一個 UserDetails 實例。UserDetails interface 定義了一些可以獲取用戶名、密碼、權限等與認證相關的資料的方法。登錄認證的時候 Spring Security 會通過 UserDetailsService 的 loadUserByUsername() 方法獲取對應的 UserDetails 進行認證,認證通過後會將該 UserDetails 放到認證通過的 Authentication 的 principal,然後再把該 Authentication 存入到 SecurityContext 中。之後如果需要使用用戶資料的時候就是通過 SecurityContextHolder 獲取存放在 SecurityContext 中的 Authentication 的 principal。
GrantedAuthority
Authentication 的 getAuthorities() 可以返回當前 Authentication 實例擁有的權限,即當前用戶擁有的權限。其返回型別是 Collection<? extends GrantedAuthority>,每一個 GrantedAuthority 實例代表賦予給當前用戶的一種權限。
SecurityContextHolder
SecurityContextHolder 是用來保存 SecurityContext,SecurityContext 中含有當前正在訪問系統的用戶的詳細資料。預設情況下,SecurityContextHolder 將使用 ThreadLocal 來保存 SecurityContext,這也就意味著在處於同一線程中的方法中我們可以從 ThreadLocal 中獲取到當前的 SecurityContext。
通過 SecurityContextHolder.getContext().getAuthentication().getPrincipal()
可以獲取到代表當前已通過認証與授權的用戶資料,這個實例通常是 UserDetails 實例。
Spring Security 底層是通過一系列的 Filter 來管理,每個 Filter 都有其自身的功能,而且各個 Filter 在功能上還有關聯關係,所以它們的順序也是非常重要的。Spring Security 對 FilterChain 中 Filter 順序有嚴格的規定的,Spring Security 對那些預設的 Filter 指定了它們的位置。
Spring Security 已經定義了一些 Filter,不管實際應用中你用到了哪些,它們應當保持如下順序。
ChannelProcessingFilter
如果你訪問的 channel 錯了,那首先就會在 channel 之間進行跳轉,如 http 變為 https。
SecurityContextPersistenceFilter
開始進行 request 的時候就可以在 SecurityContextHolder 中建立一個 SecurityContext,然後在請求結束的時候,任何對 SecurityContext 的改變都可以被 copy 到 HttpSession。
ConcurrentSessionFilter
它使用 SecurityContextHolder 的功能,而且更新對應 session 的最後更新時間,以及通過 SessionRegistry 獲取當前的 SessionInformation 以檢查當前的 session 是否已經過期,過期則會呼叫 LogoutHandler。
認證處理機制
如 UsernamePasswordAuthenticationFilter,CasAuthenticationFilter,BasicAuthenticationFilter 等,以至於 SecurityContextHolder 可以被更新為包含一個有效的 Authentication 請求。
SecurityContextHolderAwareRequestFilter
它將會把 HttpServletRequest 封裝成一個繼承自 HttpServletRequestWrapper 的 SecurityContextHolderAwareRequestWrapper,同時使用 SecurityContext 實現了 HttpServletRequest 中與安全相關的方法。
JaasApiIntegrationFilter
如果 SecurityContextHolder 中擁有的 Authentication 是一個 JaasAuthenticationToken,那麼該 Filter 將使用包含在 JaasAuthenticationToken 中的 Subject 繼續執行 FilterChain。
RememberMeAuthenticationFilter
如果之前的認證處理機制沒有更新 SecurityContextHolder,並且用戶請求包含了一個 Remember-Me 對應的 cookie,那麼一個對應的 Authentication 將會設給 SecurityContextHolder。
AnonymousAuthenticationFilter
如果之前的認證機制都沒有更新 SecurityContextHolder 擁有的 Authentication,那麼一個 AnonymousAuthenticationToken 將會設給 SecurityContextHolder。
ExceptionTransactionFilter
用於處理在 FilterChain 範圍內拋出的 AccessDeniedException 和 AuthenticationException,並把它們轉換為對應的 Http 錯誤碼返回或者對應的頁面。
FilterSecurityInterceptor,保護 Web URI,並且在訪問被拒絕時拋出異常。
認証與援權功能在下述這儿個章節
聊聊 spring security 的 permitAll 以及 ignore