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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章