1. 程式人生 > >Spring Security,沒有看起來那麼複雜(附原始碼)

Spring Security,沒有看起來那麼複雜(附原始碼)

許可權管理是每個專案必備的功能,只是各自要求的複雜程度不同,簡單的專案可能一個 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