Spring Security,沒有看起來那麼複雜(附原始碼)
阿新 • • 發佈:2021-01-27
許可權管理是每個專案必備的功能,只是各自要求的複雜程度不同,簡單的專案可能一個 Filter 或 Interceptor 就解決了,複雜一點的就可能會引入安全框架,如 Shiro, Spring Security 等。
其中 Spring Security 因其涉及的流程、類過多,看起來比較複雜難懂而被詬病。但如果能捋清其中的關鍵環節、關鍵類,Spring Security 其實也沒有傳說中那麼複雜。本文結合腳手架框架的許可權管理實現(`jboost-auth` 模組,原始碼獲取見文末),對 Spring Security 的認證、授權機制進行深入分析。
### 使用 Spring Security 認證、鑑權機制
Spring Security 主要實現了 Authentication(認證——你是誰?)、Authorization(鑑權——你能幹什麼?)
### 認證(登入)流程
Spring Security 的認證流程及涉及的主要類如下圖,
![SpringSecurity認證](https://img2020.cnblogs.com/other/632381/202101/632381-20210127120456748-1644061251.png)
認證入口為 AbstractAuthenticationProcessingFilter,一般實現有 UsernamePasswordAuthenticationFilter
1. filter 解析請求引數,將客戶端提交的使用者名稱、密碼等封裝為 Authentication,Authentication 一般實現有 UsernamePasswordAuthenticationToken
2. filter 呼叫 AuthenticationManager 的 `authenticate()` 方法對 Authentication 進行認證,AuthenticationManager 的預設實現是
ProviderManager
3. ProviderManager 認證時,委託給一個 AuthenticationProvider 列表,呼叫列表中 AuthenticationProvider 的 `authenticate()`
方法來進行認證,只要有一個通過,則認證成功,否則丟擲 AuthenticationException 異常(AuthenticationProvider 還有一個 `supports()` 方法,用來判斷該 Provider
是否對當前型別的 Authentication 進行認證)
4. 認證完成後,filter 通過 AuthenticationSuccessHandler(成功時) 或 AuthenticationFailureHandler(失敗時)來對認證結果進行處理,如返回 token 或 認證錯誤提示
### 認證涉及的關鍵類
1. 登入認證入口 UsernamePasswordAuthenticationFilter
專案中 RestAuthenticationFilter 繼承了 UsernamePasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter 將客戶端提交的引數封裝為
UsernamePasswordAuthenticationToken,供 AuthenticationManager 進行認證。
RestAuthenticationFilter 覆寫了 UsernamePasswordAuthenticationFilter 的 `attemptAuthentication(request,response)` 方法邏輯,根據
loginType 的值來將登入引數封裝到認證資訊 Authentication 中,(loginType 為 USER 時為 UsernameAuthenticationToken,
loginType 為 Phone 時為 PhoneAuthenticationToken),供下游 AuthenticationManager 進行認證。
2. 認證資訊 Authentication
使用 Authentication 的實現來儲存認證資訊,一般為 UsernamePasswordAuthenticationToken,包括
* principal:身份主體,通常是使用者名稱或手機號
* credentials:身份憑證,通常是密碼或手機驗證碼
* authorities:授權資訊,通常是角色 Role
* isAuthenticated:認證狀態,表示是否已認證
本專案中的 Authentication 實現:
* UsernameAuthenticationToken: 使用使用者名稱登入時封裝的 Authentication
* principal => username
* credentials => password
* 擴充套件了兩個屬性: uuid, code,用來驗證圖形驗證碼
* PhoneAuthenticationToken: 使用手機驗證碼登入時封裝的 Authentication
* principal => phone(手機號)
* credentials => code(驗證碼)
兩者都繼承了 UsernamePasswordAuthenticationToken。
3. 認證管理器 AuthenticationManager
認證管理器介面 AuthenticationManager,包含一個 `authenticate(authentication)` 方法。
ProviderManager 是 AuthenticationManager 的實現,管理一個 AuthenticationProvider(具體認證邏輯提供者)列表。在其 `authenticate(authentication
)` 方法中,對 AuthenticationProvider 列表中每一個 AuthenticationProvider,呼叫其 `supports(Class authentication)` 方法來判斷是否採用該
Provider 來對 Authentication 進行認證,如果適用則呼叫 AuthenticationProvider 的 `authenticate(authentication)`
來完成認證,只要其中一個完成認證,則返回。
4. 認證提供者 AuthenticationProvider
由3可知認證的真正邏輯由 AuthenticationProvider 提供,本專案的認證邏輯提供者包括
* UsernameAuthenticationProvider: 支援對 UsernameAuthenticationToken 型別的認證資訊進行認證。同時使用 PasswordRetryUserDetailsChecker
來對密碼錯誤次數超過5次的使用者,在10分鐘內限制其登入操作
* PhoneAuthenticationProvider: 支援對 PhoneAuthenticationToken 型別的認證資訊進行認證
兩者都繼承了 DaoAuthenticationProvider —— 通過 UserDetailsService 的 `loadUserByUsername(String username)` 獲取儲存的使用者資訊
UserDetails,再與客戶端提交的認證資訊 Authentication 進行比較(如與 UsernameAuthenticationToken 的密碼進行比對),來完成認證。
5. 使用者資訊獲取 UserDetailsService
UserDetailsService 提供 `loadUserByUsername(username)` 方法,可獲取已儲存的使用者資訊(如儲存在資料庫中的使用者賬號資訊)。
本專案的 UserDetailsService 實現包括
* UsernameUserDetailsService:通過使用者名稱從資料庫獲取賬號資訊
* PhoneUserDetailsService:通過手機號碼從資料庫獲取賬號資訊
6. 認證結果處理
認證成功,呼叫 AuthenticationSuccessHandler 的 `onAuthenticationSuccess(request,
response, authentication)` 方法,在 SecurityConfiguration 中注入 RestAuthenticationFilter 時進行了設定。 本專案中認證成功後,生成 jwt token返回客戶端。
認證失敗(賬號校驗失敗或過程中丟擲異常),呼叫 AuthenticationFailureHandler 的 `onAuthenticationFailure(request,
response, exception)` 方法,在 SecurityConfiguration 中注入 RestAuthenticationFilter 時進行了設定,返回錯誤資訊。
> 以上關鍵類及其關聯基本都在 SecurityConfiguration 進行配置。
7. 工具類
SecurityContextHolder 是 SecurityContext 的容器,預設使用 ThreadLocal 儲存,使得在相同執行緒的方法中都可訪問到 SecurityContext。
SecurityContext 主要是儲存應用的 principal 資訊,在 Spring Security 中用 Authentication 來表示。在
AbstractAuthenticationProcessingFilter 中,認證成功後,呼叫 `successfulAuthentication()` 方法使用 SecurityContextHolder 來儲存
Authentication,並呼叫 AuthenticationSuccessHandler 來完成後續工作(比如返回token等)。
使用 SecurityContextHolder 來獲取使用者資訊示例:
```java
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
```
### 鑑權流程
Spring Security 的鑑權(授權)有兩種實現機制:
* FilterSecurityInterceptor:通過 Filter 對 HTTP 資源的訪問進行鑑權
* MethodSecurityInterceptor:通過 AOP 對方法的呼叫進行鑑權。在 GlobalMethodSecurityConfiguration 中注入,
需要在配置類上添加註解 `@EnableGlobalMethodSecurity(prePostEnabled = true)` 使 GlobalMethodSecurityConfiguration 配置生效。
鑑權流程及涉及的主要類如下圖,
![springsecurity鑑權](https://img2020.cnblogs.com/other/632381/202101/632381-20210127120508624-430998282.png)
1. 登入完成後,一般返回 token 供下次呼叫時攜帶進行身份認證,生成 Authentication
2. FilterSecurityInterceptor 攔截器通過 FilterInvocationSecurityMetadataSource 獲取訪問當前資源需要的許可權
3. FilterSecurityInterceptor 呼叫鑑權管理器 AccessDecisionManager 的 decide 方法進行鑑權
4. AccessDecisionManager 通過 AccessDecisionVoter 列表的鑑權投票,確定是否通過鑑權,如果不通過則丟擲 AccessDeniedException 異常
5. MethodSecurityInterceptor 流程與 FilterSecurityInterceptor 類似
### 鑑權涉及的關鍵類
1. 認證資訊提取 RestAuthorizationFilter
對於前後端分離專案,登入完成後,接下來我們一般通過登入時返回的 token 來訪問介面。
在鑑權開始前,我們需要將 token 進行驗證,然後生成認證資訊 Authentication 交給下游進行鑑權(授權)。
本專案 RestAuthorizationFilter 將客戶端上報的 jwt token 進行解析,得到 UserDetails, 並對 token 進行有效性校驗,並生成
Authentication(UsernamePasswordAuthenticationToken),通過
SecurityContextHolder 存入 SecurityContext 中供下游使用。
2. 鑑權入口 AbstractSecurityInterceptor
三個實現:
* FilterSecurityInterceptor:基於 Filter 的鑑權實現,作用於 Http 介面層級。FilterSecurityInterceptor 從 SecurityMetadataSource 的實現 DefaultFilterInvocationSecurityMetadataSource 獲取要訪問資源所需要的許可權
Co