權限管理是每個專案必備的功能,只是各自要求的復雜程度不同,簡單的專案可能一個 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” 獲取原始碼地址,


[轉載請注明出處]
作者:雨歌,可以關注作者公眾號:半路雨歌

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/253417.html
標籤:其他
上一篇:十七、Linux的壓縮和解壓
