權限管理是每個項目必備的功能,只是各自要求的複雜程度不同,簡單的項目可能一個 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” 獲取源碼地址。
[轉載請註明出處]
作者:雨歌,可以關注作者公衆號:半路雨歌