許可權管理是每個專案必備的功能,只是各自要求的複雜程度不同,簡單的專案可能一個 Filter 或 Interceptor 就解決了,複雜一點的就可能會引入安全框架,如 Shiro, Spring Security 等。
其中 Spring Security 因其涉及的流程、類過多,看起來比較複雜難懂而被詬病。但如果能捋清其中的關鍵環節、關鍵類,Spring Security 其實也沒有傳說中那麼複雜。本文結合腳手架框架的許可權管理實現(jboost-auth
模組,原始碼獲取見文末),對 Spring Security 的認證、授權機制進行深入分析。
使用 Spring Security 認證、鑑權機制
Spring Security 主要實現了 Authentication(認證——你是誰?)、Authorization(鑑權——你能幹什麼?)
認證(登入)流程
Spring Security 的認證流程及涉及的主要類如下圖,
認證入口為 AbstractAuthenticationProcessingFilter,一般實現有 UsernamePasswordAuthenticationFilter
- filter 解析請求引數,將客戶端提交的使用者名稱、密碼等封裝為 Authentication,Authentication 一般實現有 UsernamePasswordAuthenticationToken
- filter 呼叫 AuthenticationManager 的
authenticate()
方法對 Authentication 進行認證,AuthenticationManager 的預設實現是
ProviderManager - ProviderManager 認證時,委託給一個 AuthenticationProvider 列表,呼叫列表中 AuthenticationProvider 的
authenticate()
方法來進行認證,只要有一個通過,則認證成功,否則丟擲 AuthenticationException 異常(AuthenticationProvider 還有一個supports()
方法,用來判斷該 Provider
是否對當前型別的 Authentication 進行認證) - 認證完成後,filter 通過 AuthenticationSuccessHandler(成功時) 或 AuthenticationFailureHandler(失敗時)來對認證結果進行處理,如返回 token 或 認證錯誤提示
認證涉及的關鍵類
- 登入認證入口 UsernamePasswordAuthenticationFilter
專案中 RestAuthenticationFilter 繼承了 UsernamePasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter 將客戶端提交的引數封裝為
UsernamePasswordAuthenticationToken,供 AuthenticationManager 進行認證。
RestAuthenticationFilter 覆寫了 UsernamePasswordAuthenticationFilter 的 attemptAuthentication(request,response)
方法邏輯,根據
loginType 的值來將登入引數封裝到認證資訊 Authentication 中,(loginType 為 USER 時為 UsernameAuthenticationToken,
loginType 為 Phone 時為 PhoneAuthenticationToken),供下游 AuthenticationManager 進行認證。
- 認證資訊 Authentication
使用 Authentication 的實現來儲存認證資訊,一般為 UsernamePasswordAuthenticationToken,包括
- principal:身份主體,通常是使用者名稱或手機號
- credentials:身份憑證,通常是密碼或手機驗證碼
- authorities:授權資訊,通常是角色 Role
- isAuthenticated:認證狀態,表示是否已認證
本專案中的 Authentication 實現:
-
UsernameAuthenticationToken: 使用使用者名稱登入時封裝的 Authentication
- principal => username
- credentials => password
- 擴充套件了兩個屬性: uuid, code,用來驗證圖形驗證碼
-
PhoneAuthenticationToken: 使用手機驗證碼登入時封裝的 Authentication
- principal => phone(手機號)
- credentials => code(驗證碼)
兩者都繼承了 UsernamePasswordAuthenticationToken。
- 認證管理器 AuthenticationManager
認證管理器介面 AuthenticationManager,包含一個 authenticate(authentication)
方法。
ProviderManager 是 AuthenticationManager 的實現,管理一個 AuthenticationProvider(具體認證邏輯提供者)列表。在其 authenticate(authentication )
方法中,對 AuthenticationProvider 列表中每一個 AuthenticationProvider,呼叫其 supports(Class<?> authentication)
方法來判斷是否採用該
Provider 來對 Authentication 進行認證,如果適用則呼叫 AuthenticationProvider 的 authenticate(authentication)
來完成認證,只要其中一個完成認證,則返回。
- 認證提供者 AuthenticationProvider
由3可知認證的真正邏輯由 AuthenticationProvider 提供,本專案的認證邏輯提供者包括
- UsernameAuthenticationProvider: 支援對 UsernameAuthenticationToken 型別的認證資訊進行認證。同時使用 PasswordRetryUserDetailsChecker
來對密碼錯誤次數超過5次的使用者,在10分鐘內限制其登入操作 - PhoneAuthenticationProvider: 支援對 PhoneAuthenticationToken 型別的認證資訊進行認證
兩者都繼承了 DaoAuthenticationProvider —— 通過 UserDetailsService 的 loadUserByUsername(String username)
獲取儲存的使用者資訊
UserDetails,再與客戶端提交的認證資訊 Authentication 進行比較(如與 UsernameAuthenticationToken 的密碼進行比對),來完成認證。
- 使用者資訊獲取 UserDetailsService
UserDetailsService 提供 loadUserByUsername(username)
方法,可獲取已儲存的使用者資訊(如儲存在資料庫中的使用者賬號資訊)。
本專案的 UserDetailsService 實現包括
- UsernameUserDetailsService:通過使用者名稱從資料庫獲取賬號資訊
- PhoneUserDetailsService:通過手機號碼從資料庫獲取賬號資訊
- 認證結果處理
認證成功,呼叫 AuthenticationSuccessHandler 的 onAuthenticationSuccess(request, response, authentication)
方法,在 SecurityConfiguration 中注入 RestAuthenticationFilter 時進行了設定。 本專案中認證成功後,生成 jwt token返回客戶端。
認證失敗(賬號校驗失敗或過程中丟擲異常),呼叫 AuthenticationFailureHandler 的 onAuthenticationFailure(request, response, exception)
方法,在 SecurityConfiguration 中注入 RestAuthenticationFilter 時進行了設定,返回錯誤資訊。
以上關鍵類及其關聯基本都在 SecurityConfiguration 進行配置。
- 工具類
SecurityContextHolder 是 SecurityContext 的容器,預設使用 ThreadLocal 儲存,使得在相同執行緒的方法中都可訪問到 SecurityContext。
SecurityContext 主要是儲存應用的 principal 資訊,在 Spring Security 中用 Authentication 來表示。在
AbstractAuthenticationProcessingFilter 中,認證成功後,呼叫 successfulAuthentication()
方法使用 SecurityContextHolder 來儲存
Authentication,並呼叫 AuthenticationSuccessHandler 來完成後續工作(比如返回token等)。
使用 SecurityContextHolder 來獲取使用者資訊示例:
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 配置生效。
鑑權流程及涉及的主要類如下圖,
- 登入完成後,一般返回 token 供下次呼叫時攜帶進行身份認證,生成 Authentication
- FilterSecurityInterceptor 攔截器通過 FilterInvocationSecurityMetadataSource 獲取訪問當前資源需要的許可權
- FilterSecurityInterceptor 呼叫鑑權管理器 AccessDecisionManager 的 decide 方法進行鑑權
- AccessDecisionManager 通過 AccessDecisionVoter 列表的鑑權投票,確定是否通過鑑權,如果不通過則丟擲 AccessDeniedException 異常
- MethodSecurityInterceptor 流程與 FilterSecurityInterceptor 類似
鑑權涉及的關鍵類
- 認證資訊提取 RestAuthorizationFilter
對於前後端分離專案,登入完成後,接下來我們一般通過登入時返回的 token 來訪問介面。
在鑑權開始前,我們需要將 token 進行驗證,然後生成認證資訊 Authentication 交給下游進行鑑權(授權)。
本專案 RestAuthorizationFilter 將客戶端上報的 jwt token 進行解析,得到 UserDetails, 並對 token 進行有效性校驗,並生成
Authentication(UsernamePasswordAuthenticationToken),通過
SecurityContextHolder 存入 SecurityContext 中供下游使用。
- 鑑權入口 AbstractSecurityInterceptor
三個實現:
- FilterSecurityInterceptor:基於 Filter 的鑑權實現,作用於 Http 介面層級。FilterSecurityInterceptor 從 SecurityMetadataSource 的實現 DefaultFilterInvocationSecurityMetadataSource 獲取要訪問資源所需要的許可權
Collection,然後呼叫 AccessDecisionManager 進行授權決策投票,若投票通過,則允許訪問資源,否則將禁止訪問。 - MethodSecurityInterceptor:基於 AOP 的鑑權實現,作用於方法層級。
- AspectJMethodSecurityInterceptor:用來支援 AspectJ JointPoint 的 MethodSecurityInterceptor
- 獲取資源許可權資訊 SecurityMetadataSource
SecurityMetadataSource 讀取訪問資源所需的許可權資訊,讀取的內容,就是我們配置的訪問規則,如我們在配置類中配置的訪問規則:
@Override
protected void configure(HttpSecurity http) throws Exception{
http.authorizeRequests()
.antMatchers(excludes).anonymous()
.antMatchers("/api1").hasAuthority("permission1")
.antMatchers("/api2").hasAuthority("permission2")
...
}
我們可以自定義一個 SecurityMetadataSource 來從資料庫或其它儲存中獲取資源許可權規則資訊。
- 鑑權管理器 AccessDecisionManager
AccessDecisionManager 介面的 decide(authentication, object, configAttributes)
方法對本次請求進行鑑權,其中
- authentication:本次請求的認證資訊,包含 authority(如角色) 資訊
- object:當前被呼叫的被保護物件,如介面
- configAttributes:與被保護物件關聯的配置屬性,表示要訪問被保護物件需要滿足的條件,如角色
AccessDecisionManager 介面的實現者鑑權時,最終是通過呼叫其內部 List<AccessDecisionVoter<?>>
列表中每一個元素的 vote(authentication, object, attributes)
方法來進行的,根據決策的不同分為如下三種實現
- AffirmativeBased:一票通過權策略。只要有一個 AccessDecisionVoter 通過(
AccessDecisionVoter.vote
返回 AccessDecisionVoter.
ACCESS_GRANTED),則鑑權通過。為預設實現 - ConsensusBased:少數服從多數策略。多數 AccessDecisionVoter 通過,則鑑權通過,如果贊成票與反對票相等,則根據變數 allowIfEqualGrantedDeniedDecisions
的值來決定,該值預設為 true - UnanimousBased:全票通過策略。所有 AccessDecisionVoter 通過或棄權(返回 AccessDecisionVoter.
ACCESS_ABSTAIN),無一反對則通過,只要有一個反對就拒絕;如果全部棄權,則根據變數 allowIfAllAbstainDecisions 的值來決定,該值預設為 false
- 鑑權投票者 AccessDecisionVoter
與 AuthenticationProvider 類似,AccessDecisionVoter 也包含 supports(attribute)
方法(是否採用該 Voter 來對請求進行鑑權投票) 與 vote (authentication, object, attributes)
方法(具體的鑑權投票邏輯)
FilterSecurityInterceptor 的 AccessDecisionManager 的投票者列表(AbstractInterceptUrlConfigurer.createFilterSecurityInterceptor() 中設定)包括:
- WebExpressionVoter:驗證 Authentication 的 authenticated。
MethodSecurityInterceptor 的 AccessDecisionManager 的投票者列表(GlobalMethodSecurityConfiguration.accessDecisionManager()
中設定)包括:
- PreInvocationAuthorizationAdviceVoter: 如果 @EnableGlobalMethodSecurity 註解開啟了 prePostEnabled,則新增該 Voter,對使用了 @PreAuthorize 註解的方法進行鑑權投票
- Jsr250Voter:如果 @EnableGlobalMethodSecurity 註解開啟了 jsr250Enabled,則新增該 Voter,對 @Secured 註解的方法進行鑑權投票
- RoleVoter:總是新增, 如果
ConfigAttribute.getAttribute()
以ROLE_
開頭,則參與鑑權投票 - AuthenticatedVoter:總是新增,如果
ConfigAttribute.getAttribute()
值為
IS_AUTHENTICATED_FULLY
,IS_AUTHENTICATED_REMEMBERED
,IS_AUTHENTICATED_ANONYMOUSLY
其中一個,則參與鑑權投票
- 鑑權結果處理
ExceptionTranslationFilter 異常處理 Filter, 對認證鑑權過程中丟擲的異常進行處理,包括:
- authenticationEntryPoint: 對過濾器鏈中丟擲 AuthenticationException 或 AccessDeniedException 但 Authentication 為
AnonymousAuthenticationToken 的情況進行處理。如果 token 校驗失敗,如 token 錯誤或過期,則通過 ExceptionTranslationFilter 的 AuthenticationEntryPoint 進行處理,本專案使用 RestAuthenticationEntryPoint 來返回統一格式的錯誤資訊 - accessDeniedHandler: 對過濾器鏈中丟擲 AccessDeniedException 但 Authentication 不為 AnonymousAuthenticationToken 的情況進行處理,本專案使用 RestAccessDeniedHandler 來返回統一格式的錯誤資訊
如果是 MethodSecurityInterceptor 鑑權時丟擲 AccessDeniedException,並且通過 @RestControllerAdvice 提供了統一異常處理,則將由統一異常處理類處理,因為
MethodSecurityInterceptor 是 AOP 機制,可由 @RestControllerAdvice 捕獲。
本專案中, RestAuthorizationFilter 在 Filter 鏈中位於 ExceptionTranslationFilter 的前面,所以其中丟擲的異常也不能被 ExceptionTranslationFilter 捕獲, 由 cn.jboost.base.starter.web.ExceptionHandlerFilter 捕獲處理。
也可以將 RestAuthorizationFilter 放入 ExceptionTranslationFilter 之後,但在 RestAuthorizationFilter 中需要對 SecurityContextHolder.getContext().getAuthentication()
進行 AnonymousAuthenticationToken 的判斷,因為 AnonymousAuthenticationFilter 位於 ExceptionTranslationFilter 前面,會對 Authentication 為空的請求生成一個
AnonymousAuthenticationToken,放入 SecurityContext 中。
總結
安全框架一般包括認證與授權兩部分,認證解決你是誰的問題,即確定你是否有合法的訪問身份,授權解決你是否有許可權訪問對應資源的問題。Spring Security 使用 Filter 來實現認證,使用 Filter(介面層級) + AOP(方法層級)的方式來實現授權。本文相對偏理論,但也結合了腳手架中的實現,對照檢視,應該更易理解。
本文基於 Spring Boot 腳手架中的許可權管理模組編寫,該腳手架提供了前後端分離的許可權管理實現,效果如下圖,可關注作者公眾號 “半路雨歌”,回覆 “jboost” 獲取原始碼地址。
[轉載請註明出處]
作者:雨歌,可以關注作者公眾號:半路雨歌