博客 / 詳情

返回

Spring Security 6.x 淺談身份認證的架構設計

一、基本概念

“Authentication(認證)”是spring security框架中最重要的功能之一,所謂認證,就是對當前訪問系統的用户給予一個合法的身份標識,用户只有通過認證後才可以進入系統,在物理世界中,有點類似“拿工卡刷門禁”的場景。
身份認證在市面上有很多種的實現協議,最常見的就是用户名密碼的認證方式,另外還有OAuth2.0,CAS(Central Authentication Service),SAML等,其中OAuth2.0是一種我們比較熟悉的認證協議,例如微信,支付寶提供的第三方登錄。
回到身份認證的原本需求:

  • 首先系統要提供對應的認證服務,即需要判斷用户提交的憑證是否正確,憑證是一個比較寬泛的概念,密碼只是其中一種,還包括短信驗證碼,指紋等,一切可以證明“你是你”的材料都可以是憑證
  • 在用户認證成功後,系統還要記錄這些認證信息,並返回客户端一個令牌,對於後續的請求,通過這個令牌就可以校驗是否經過認證,若已經完成過認證,那麼應該取出當時認證的信息,包括用户名,權限等,然後繼續執行後續的業務邏輯,若沒有認證信息,則拒絕訪問。這樣才能對受保護的系統資源起到作用。
    根據上面的描述,很自然地,我們想到定義一個controller的API接口來提供認證服務,然後定義一個“切面”來校驗認證信息,這種方式可以方便地攔截到系統內各個資源的訪問請求,不僅可以靈活配置,也不會侵入業務代碼。

到此,我們對認證的架構有了一個初步的構想,先畫一個簡單的草稿
image.png
這裏所謂的“令牌”,“憑證”,“認證信息”,“受保護資源”都是抽象的概念,並不特指某一種實現,“切面”也不是Spring的AOP,只表示在執行校驗邏輯時,不與受保護資源相耦合,它應該是獨立運作的模塊。
下面具體看一下spring security中的認證架構設計,對比上圖,學習一下spring security是如何實踐的。

二、架構設計

spring security利用了SecurityFilterChain的過濾器中實現了校驗邏輯,另外為了實現各種認證協議,spring security也內置了很多種認證實現類,供開發者直接使用,不過這裏提供兩種方式,一種也是利用SecurityFilterChain的過濾器來實現認證服務,當然也可以實現自定義的Controller來暴露API接口。明確了這兩點之後,我們再給出spring security完整的認證架構,圖中均以SecurityFilterChain的過濾器實現認證和校驗的邏輯,這是比較常見慣用的方法。

可以參考官方文檔 https://docs.spring.io/spring-security/reference/servlet/auth...,不過官方文檔的結構組織比較散,這裏我們再做一次整合,看起來更直觀一些

image.png

首先介紹一下相關的接口和類:

接口

  • Authentication:頂層接口,用於保存身份認證信息,主要包括3個部分:用户標識(principal,通常為用户名),憑證(credentials,通常為密碼),權限信息(authorities,通常為該用户所擁有的角色)
  • SecurityContext:頂層接口,直譯為安全上下文,內部只定義了getAuthentication和setAuthentication兩個方法,概括地説,SecurityContext相當於用於裝載Authentication對象的容器,在整個SecurityFilterChain中,為不同的認證機制操作Authentication對象時提供服務。
  • AuthenticationManager: 頂層接口,定義了“認證“方法,簽名如下:
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
  • AuthenticationProvider: 頂層接口,同樣也定義了一個簽名相同的“認證”方法,不同於AuthenticationManager的認證方法,這個才是各種認證協議的具體實現,它通常接受一個未認證的Authentication對象的參數,該對象僅包含了principal和credentials的信息,在經過認證後,會把authorities填充進來,並將狀態設置為已認證。在spring security中內置了很多實現類,例如OAuth2LoginAuthenticationProvider,用於實現OAuth2.0認證協議等。當然我們也可以根據需要自定義其實現。
  • SecurityContextRepository:頂層接口,定義了保存和加載SecuriyContext對象的方法,常用的實現有HttpSessionSecurityContextRepository,即通過request的會話對象session,存取SecurityContext的實例。
  • SecurityContextHolderStrategy:頂層接口,定義了在當前請求的線程中,獲取和設置SecurityContext對象等方法,在5.8版本之後,新增了兩個get/set“延遲(Deferred)”接口,主要是使用了Supplier函數式接口實現的惰性計算,不過只是性能上的考量,本質上都是用於維護SecurityContext對象的方法

  • SecurityContextHolder:它是spring security認證模型中最為常用的一個工具類,它採用策略模式封裝了SecurityContextHolderStrategy接口實現,默認的策略實現為ThreadLocalSecurityContextHolderStrategy,其底層使用了ThreadLocal實現對SecurityContext對象的存取邏輯,這樣可以保證在一次請求的同一個線程中,方便地獲取SecurityContext對象。
  • ProviderManager: AuthenticationManager的實現類,它內部維護了一個List<AuthenticationProvider>成員變量,在實現AuthenticationManager#authenticate方法時,其實是遍歷這個List<AuthenticationProvider>列表,依次判斷是否支持當前Authentication對象(如OAuth2LoginAuthenticationProvider支持OAuth2LoginAuthenticationToken),如果支持,則調用AuthenticationProvider#authenticate方法,完成認證過程。

從圖中可以看到,整個認證流程主要圍繞以下3個Filter:

  1. SecurityContextHolderFilter:它在整個SecurityFilterChain中具有較高的優先級,因為當一個請求進入SecurityFilterChain的時候,需要從SecurityContextRepository加載SecurityContext實例①,並調用SecurityContextHolder對應的set方法進行保存②,以便後續其他地方獲取這個SecurityContext實例,如上文所述,通常會保存在ThreadLocal中
  2. AuthorizationFilter:如果該請求沒有被認證過,那麼在當前的SecurityContext對象中是沒有Authentication實例的,這時在執行AuthorizationFilter的邏輯時就會發生異常,AuthorizationFilter主要是用來判斷請求訪問受保護資源時,是否符合授權條件,而為了獲取用户的授權信息,先通過SecurityContext得到Authentication認證信息①,這時如果獲取到Authentication實例為空,就表示該請求並沒有認證過,那麼就會拋出一個AuthenticationCredentialsNotFoundException的異常②,這個異常會被ExceptionTranslationFilter捕獲,通常情況下,異常處理方式就是跳轉到到登錄頁面③,讓用户完成登錄的操作。
  3. .AbstractAuthenticationProcessingFilter:它定義了一個比較通用的認證“模板”方法。當用户發起登錄請求時,AbstractAuthenticationProcessingFilter配置的RequestMatcher就匹配到這次請求的url,默認執行認證的是UsernamePasswordAuthenticationFilter,它匹配的請求端點是"/login",此時它從request請求參數中獲取用户名和密碼,並封裝成UsernamePasswordAuthenticationToken①,然後交給ProviderManager#authenticate方法對其認證②,在認證通過之後,我們將AuthenticationProvider返回的Authentication對象③,此時SecurityContextHolderStrategy會創建出一個空載的SecurityContext實例④,並傳入上述Authentication⑤,然後調用SecurityContextHolderStrategy的保存方法⑤,最後通過SecurityContextRepository進行持久化⑦,可以參考以下的樣板代碼,對於各類認證實現,基本上大同小異。
try {
    Authentication authenticationToken = createAuthentication() // // 例如創建UsernamePasswordAuthenticationToken,OAuth2AuthorizationCodeAuthenticationToken等等,將待認證的信息封裝起來,
    Authentication authResult = this.authenticationManager.authenticate(someAuthenticationToken); // 交給ProviderManager進行認證,通常由實際的AuthenticationProvider實現類完成具體的認證邏輯,並將認證結果返回
    SecurityContext context = this.securityContextRepository.createEmptyContext(); // 創建一個空載的SecurityContext實例
    context.setAuthentication(authResult); // 傳入經過認證的Authentication對象
    this.securityContextHolderStrategy.setContext(context); // 存儲SecurityContextHolder中,方便同一個線程執行過程中的其他地方獲取
    this.securityContextRepository.saveContext(context, request, response); // 進行持久化,方便下次請求訪問時,可以獲取對應SecurityContext,實現登錄態的保持
    this.successHandler.onAuthenticationSuccess(request, response, authResult); // 認證成功後的流程,例如跳轉到系統首頁等
} catch (AuthenticationException ex) {
    // Authentication failed
    this.securityContextHolderStrategy.clearContext(); // 認證失敗時,清空SecurityContext
    this.failureHandler.onAuthenticationFailure(request, response, failed); // 認證失敗後的流程,例如提示錯誤信息等
}

説明:spring security對用户名和密碼的認證提供了默認實現DaoAuthenticationProvider,但由於默認實現限制比較多,一般在實際的生產活動中不會採用,通常會繼承AbstractUserDetailsAuthenticationProvider來定製開發,或者參考它的源碼自定義實現AuthenticationProvider接口。

三、總結

最後,我們對spring security整個認證架構中的認證流程和存取校驗流程,再做一個總結:

  • 認證流程:AuthenticationManager為這個系統所支持的所有認證協議,統一提供authenticate方法,比如支持用户名密碼登錄,也支持短信驗證碼,第三方授權登錄等,不論哪種認證請求,最終都交由這個方法執行,其實現類ProviderManager則高度封裝了認證過程,其中具體的AuthenticationProvider實現維護在List列表中,通過遍歷使得不同的認證協議進入不同的認證實現類,然後都返回Authentication對象,Authentication定義了一個認證信息應該必須包含的信息,包括用户標識principal,憑證credentials,權限authorities,因此我們也可以實現自定義的AuthenticationProvider,並註冊到ProviderManager中,然後再實現自定義的認證Filter和Authentication,這樣就完成了整合。
  • 存取校驗流程:在得到認證後的Authentication對象,需要解決的是如何獲取這個Authentication對象,以判斷該請求是否已經通過認證,這裏就引入另一個重要的類SecurityContext,它相當於一個用於裝載Authentication對象的容器,首先依賴SecurityContextRepository從持久化的介質(例如session)中加載出來SecurityContext對象,其次通過SecurityContextHolder內部策略類方便快速地讀寫SecurityContext對象,這裏很容易就想到使用ThreadLocal來實現同一個請求的線程中存取操作,spring security也是這麼做的,最終在得到SecurityContext後,可以通過其內部的Authentication對象判斷是否已認證。

可見,上述兩個核心流程基本圍繞着Authentication和SecurityContext這兩個接口來建設,前者對外提供認證服務,我們可以進行深度的定製開發,包括Authentication,Filter,AuthenticationProvider都可以自定義實現,並整合進入SecurityFilterChain,後者對內提供存取服務,通常情況下我們也不會對存取流程進行改造,對於絕大多數場景,只需要利用SecurityContextHolder這個工具類讀寫SecurityContext對象,這基本上已經足夠了。這樣的設計,在最大程度上固化了存取校驗的邏輯,不會因為認證機制和結果的不同,而改變存取校驗的邏輯。

image.png

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.