Spring Security,沒有看起來那么復雜(附源碼)


權限管理是每個項目必備的功能,只是各自要求的復雜程度不同,簡單的項目可能一個 Filter 或 Interceptor 就解決了,復雜一點的就可能會引入安全框架,如 Shiro, Spring Security 等。
其中 Spring Security 因其涉及的流程、類過多,看起來比較復雜難懂而被詬病。但如果能捋清其中的關鍵環節、關鍵類,Spring Security 其實也沒有傳說中那么復雜。本文結合腳手架框架的權限管理實現(jboost-auth 模塊,源碼獲取見文末),對 Spring Security 的認證、授權機制進行深入分析。

使用 Spring Security 認證、鑒權機制

Spring Security 主要實現了 Authentication(認證——你是誰?)、Authorization(鑒權——你能干什么?)

認證(登錄)流程

Spring Security 的認證流程及涉及的主要類如下圖,

SpringSecurity認證

認證入口為 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 進行認證。

  1. 認證信息 Authentication

使用 Authentication 的實現來保存認證信息,一般為 UsernamePasswordAuthenticationToken,包括

  • principal:身份主體,通常是用戶名或手機號
  • credentials:身份憑證,通常是密碼或手機驗證碼
  • authorities:授權信息,通常是角色 Role
  • isAuthenticated:認證狀態,表示是否已認證

本項目中的 Authentication 實現:

  • UsernameAuthenticationToken: 使用用戶名登錄時封裝的 Authentication

    • principal => username
    • credentials => password
    • 擴展了兩個屬性: uuid, code,用來驗證圖形驗證碼
  • PhoneAuthenticationToken: 使用手機驗證碼登錄時封裝的 Authentication

    • principal => phone(手機號)
    • credentials => code(驗證碼)

兩者都繼承了 UsernamePasswordAuthenticationToken。

  1. 認證管理器 AuthenticationManager

認證管理器接口 AuthenticationManager,包含一個 authenticate(authentication) 方法。
ProviderManager 是 AuthenticationManager 的實現,管理一個 AuthenticationProvider(具體認證邏輯提供者)列表。在其 authenticate(authentication ) 方法中,對 AuthenticationProvider 列表中每一個 AuthenticationProvider,調用其 supports(Class<?> authentication) 方法來判斷是否采用該
Provider 來對 Authentication 進行認證,如果適用則調用 AuthenticationProvider 的 authenticate(authentication)
來完成認證,只要其中一個完成認證,則返回。

  1. 認證提供者 AuthenticationProvider

由3可知認證的真正邏輯由 AuthenticationProvider 提供,本項目的認證邏輯提供者包括

  • UsernameAuthenticationProvider: 支持對 UsernameAuthenticationToken 類型的認證信息進行認證。同時使用 PasswordRetryUserDetailsChecker
    來對密碼錯誤次數超過5次的用戶,在10分鍾內限制其登錄操作
  • PhoneAuthenticationProvider: 支持對 PhoneAuthenticationToken 類型的認證信息進行認證

兩者都繼承了 DaoAuthenticationProvider —— 通過 UserDetailsService 的 loadUserByUsername(String username) 獲取保存的用戶信息
UserDetails,再與客戶端提交的認證信息 Authentication 進行比較(如與 UsernameAuthenticationToken 的密碼進行比對),來完成認證。

  1. 用戶信息獲取 UserDetailsService

UserDetailsService 提供 loadUserByUsername(username) 方法,可獲取已保存的用戶信息(如保存在數據庫中的用戶賬號信息)。

本項目的 UserDetailsService 實現包括

  • UsernameUserDetailsService:通過用戶名從數據庫獲取賬號信息
  • PhoneUserDetailsService:通過手機號碼從數據庫獲取賬號信息
  1. 認證結果處理

認證成功,調用 AuthenticationSuccessHandler 的 onAuthenticationSuccess(request, response, authentication) 方法,在 SecurityConfiguration 中注入 RestAuthenticationFilter 時進行了設置。 本項目中認證成功后,生成 jwt token返回客戶端。

認證失敗(賬號校驗失敗或過程中拋出異常),調用 AuthenticationFailureHandler 的 onAuthenticationFailure(request, response, exception) 方法,在 SecurityConfiguration 中注入 RestAuthenticationFilter 時進行了設置,返回錯誤信息。

以上關鍵類及其關聯基本都在 SecurityConfiguration 進行配置。

  1. 工具類

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 配置生效。

鑒權流程及涉及的主要類如下圖,

springsecurity鑒權

  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 中供下游使用。

  1. 鑒權入口 AbstractSecurityInterceptor

三個實現:

  • FilterSecurityInterceptor:基於 Filter 的鑒權實現,作用於 Http 接口層級。FilterSecurityInterceptor 從 SecurityMetadataSource 的實現 DefaultFilterInvocationSecurityMetadataSource 獲取要訪問資源所需要的權限
    Collection ,然后調用 AccessDecisionManager 進行授權決策投票,若投票通過,則允許訪問資源,否則將禁止訪問。
  • MethodSecurityInterceptor:基於 AOP 的鑒權實現,作用於方法層級。
  • AspectJMethodSecurityInterceptor:用來支持 AspectJ JointPoint 的 MethodSecurityInterceptor
  1. 獲取資源權限信息 SecurityMetadataSource

SecurityMetadataSource 讀取訪問資源所需的權限信息,讀取的內容,就是我們配置的訪問規則,如我們在配置類中配置的訪問規則:

@Override
protected void configure(HttpSecurity http) throws Exception{
    http.authorizeRequests()
        .antMatchers(excludes).anonymous()
        .antMatchers("/api1").hasAuthority("permission1")
        .antMatchers("/api2").hasAuthority("permission2")
        ...
}

我們可以自定義一個 SecurityMetadataSource 來從數據庫或其它存儲中獲取資源權限規則信息。

  1. 鑒權管理器 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
  1. 鑒權投票者 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_FULLYIS_AUTHENTICATED_REMEMBEREDIS_AUTHENTICATED_ANONYMOUSLY 其中一個,則參與鑒權投票
  1. 鑒權結果處理

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” 獲取源碼地址。

jboost-admin-login
jboost-admin-main


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM