Spring Security OAuth2 完全解析 (流程/原理/實戰定制) —— Client / ResourceServer 篇


一、前言

假設對 Spring Security 本身原理有一定程度的了解,對 OAuth2 規范流程、Jwt 有基礎了解,以此來對 SpringSecurity 整合 OAuth2 有個快速全面的認識。
(關於總體流程,若對SS實在不熟悉可以簡單理解為:Filter構造Authentication-> Provider認證並填充-> 設置到SecurityContext -> 而后用於Filter/AOP鑒權)

要了解一個SpringSecurity模塊,就是了解它如何身份認證、如何自定義、(至於如何鑒權是SpringSecurity通用部分),對應到代碼就是:背后的相關 Configurer、Filter、Authentication、Provider。

本文以最新Spring Boot版本2.6.2 以授權碼模式梳理;涉及源碼太多,就不放源碼對照了,可以自行fork查看;斜體表示可配置自定義替換的部分

  • 第一部分:先演示默認配置下 spring-boot-starter-oauth2-client 所帶來的流程和效果,建立大概認知。對應代碼 thirdpart-login 項目
  • 第二部分:全面解析 oauth2login、oauth2client 原理。
  • 第三部分:常見業務下我們自己用戶系統也有token分發需求,因此也解讀下提供JWT服務的 oauth2ResourceServer 模塊
  • 第四部分:綜上所述,實戰定制SpringSecurity OAuth2。對應代碼 thirdpart-login-custom

二、默認 spring-boot-starter-oauth2-client 效果預覽

OAuth2ClientAutoConfiguration自動配置類引入的默認配置,可由代碼 thirdpart-login 復現。

  1. 后端在 .yml 配置中做好相關配置
  2. 訪問受限資源"/user",后端鑒權異常后,由LoginUrlAuthenticationEntryPoint重定向到登錄頁。
  3. 登錄頁則由DefaultLoginPageGeneratingFilter根據相關配置自動構造頁面String返回。
  4. 頁面點擊<a "href"="/oauth2/authorization/gitee">發出授權請求。
    后端OAuth2AuthorizationRequestRedirectFilter匹配響應該模板路徑,返回實際授權碼請求的重定向響應,轉入三方授權頁面:
  5. 同意授權后,Gitee會向游覽器返回重定向響應。
    游覽器向 "redirect-uri" 發起訪問,此時被后端OAuth2LoginAuthenticationFilter匹配處理,其會用請求攜帶的 code 向配置的 "token-uri、user-info-uri" 發起一系列請求,最后構造出認證后的身份放入SecurityContext,以SESSION持久化等。再將先前保存在SESSION中的的受限資源訪問請求拿出,重定向重新訪問。

三、oauth2Login、oauth2Client 解析

1. 兩者區別

這兩者都是在SpringSecurity中整合OAuth2的入口方法(例http.oauth2Login()),對應OAuth2LoginConfigurerOAuth2ClientConfigurer,只是引入Filter有所異同。簡而言之:

  • oauth2Login會在授權請求時進行認證(即設置安全上下文SecurityContext),背后會連續訪問acc_token&user-info-url 將獲取的用戶信息構造填充 Authentication。
  • oauth2Client也會對授權請求進行處理,但只是獲取到access_token后用repository存起來(要怎么使用自行處理),不會認證,這也意味着需要自行實現認證邏輯。

2. 從 OAuth2 請求的維度概覽

  • 對 授權碼code 的請求:由OAuth2AuthorizationRequestRedirectFilter響應"授權請求"向客戶端返回重定向響應,定向到實際 "authorization-uri"
  • 對 access_token 和 user-info-uri 實際請求:OAuth2LoginAuthenticationFilter會對回調地址(攜帶了code和state)進行處理,調用AuthemticationManager進行認證。背后OAuth2LoginAuthenticationProvider會進行連續 token-uri、user-info-uri 請求,最后返回完全填充的OAuth2LoginAuthenticationToken

3. 必須要配置的屬性:

太長就不貼了 參考Github項目代碼CommonOAuth2Provider也內建了一些常見OAuth2提供方,在之內少配幾個字段也沒關系。

  • Client屬性:OAuth2ClientRegistrationRepositoryConfiguration用以處理application.yml中的相關屬性,並構建代表OAuth2方的一個個ClientRegistration。根據不同模式,對必須屬性有不同要求。
  • provider屬性:除了上圖授權碼模式下必須校驗的 "authorization-uri、token-uri" 外,"user-info-uri"、userNameAttribute也是必須的,在后續OAuth2LoginAuthenticationProvider調用的DefaultOAuth2UserService內,必須需要這倆屬性才能嘗試訪問 user-info-uri 並包裝為DefaultOAuth2User

4. 相關 Authentication

  1. OAuth2LoginAuthenticationToken
    用以給Provider認證過渡用,最初僅含code,最終包含access_token、user等。
  2. OAuth2AuthorizationCodeAuthenticationToken
    用以給Provider認證過渡用,未填充時僅含code,經填充后包含access_token等。
  3. OAuth2AuthenticationToken
    authenticated=true 認證后安全上下文實際保存的OAuth2用戶認證,由convert將填充后的OAuth2LoginAuthenticationToken轉換而來。

5. 相關 Filter

  1. OAuth2AuthorizationRequestRedirectFilter

    1. 通過調用OAuth2AuthorizationRequestResolver用於判斷是否為授權請求(默認為 "/oauth2/authorization/{registrationId}",可通過.oauth2Login().authorizationEndpoint().baseUri()配置) ,並且請求包裝為OAuth2AuthorizationRequest后由authorizationRequestRepository(默認基於SESSION實現)將授權請求保存(后有他用)
    2. 隨后重定向到追加了參數(client-id、response_type)的真實授權碼請求。
  2. OAuth2LoginAuthenticationFilter
    繼承自AbstractAuthenticationProcessingFilter,即負責身份認證的Filter。

    1. 當是loginProcessingUrl(默認為/login/oauth2/code/*)請求且帶了code和state時,嘗試以這倆參數構建OAuth2LoginAuthenticationToken且調用AuthenticationManager去進行認證。
    2. 認證通過后,調用authenticationResultConverter將認證后完全填充的OAuth2LoginAuthenticationToken轉為authenticated=true的OAuth2AuthenticationToken,用以代表認證后的身份。(該converter默認就是直接提取填充后的"principal、authorities、clientid"直接new)
    3. 將先前得到 "token、refreshToken" 等信息包裝為OAuth2AuthorizedClient調用 OAuth2AuthorizedClientRepository#saveAuthorizedClient保存起來(默認是基於內存實現的ClientId和Principal為key的Map)
  3. OAuth2AuthorizationCodeGrantFilter
    (該Filter,在oauth2Login()下會永遠被跳過,因為該請求已被OAuth2LoginAuthenticationFilter處理后通過successHandler重定向)
    匹配帶code與state的請求(表示回調請求)且滿足authorizationRequestRepository.loadAuthorizationRequest不為空時(表示經過了RedirectFilter,是先前授權請求發起的),會構造 OAuth2AuthorizationCodeAuthenticationToken交由AuthenticationManager(背后交由OAuth2AuthorizationCodeAuthenticationProvider)進行認證,並將結果構造為OAuth2AuthorizedClient交由authorizedClientRepository保存,然后去除參數再將請求重定向到 "savedRequest 或者 redirect-url"。
    【注:不是很能理解該Filter這里為什么要重定向,這個重定向真的很惱火。如果API自身需要code,這重定向把參數清除了會報錯;而即便API不要code了依附於它的邏輯使用authorizedClientRepository,那也是無意義多一次請求。而且其基於SESSION的實現本來沒什么問題,但非要重定向請求一次就導致單純的多實例時會存在問題】

6. 相關 Provider

  1. OAuth2LoginAuthenticationProvider

    1. OAuth2LoginAuthenticationToken嘗試認證,其內會進一步構造OAuth2AuthorizationCodeAuthenticationToken,然后調用 OAuth2AuthorizationCodeAuthenticationProvider 對其進行認證。
    2. 經過上述認證后拿到填充了 "access_token" 的OAuth2AuthorizationCodeAuthenticationToken,會構造成OAuth2UserRequest后傳給OAuth2UserService負責進行實際的 "user-info-uri" 請求,並將結果包裝成DefaultOAuth2User返回。(該User擁有兩類authorities,一個是ROLE_USER(Spring在經過oauth2UserService時手動添加的),一類是Token中的SCOPE_{sopces})
  2. OAuth2AuthorizationCodeAuthenticationProvider
    OAuth2AuthorizationCodeAuthenticationToken嘗試認證,內部會構造對"token-uri"的實際請求,並調用DefaultAuthorizationCodeTokenResponseClient進行請求返回,並根據返回結果OAuth2AccessTokenResponse(內含access_token/refreash_token),新new一個填充了"access_token"的OAuth2AuthorizationCodeAuthenticationToken返回。

四、oauth2ResourceServer(JWT)

1. 概述

org.springframework.boot:spring-boot-starter-oauth2-resource-server 引入,提供對請求中攜帶token校驗解析、身份認證的服務。

2. 必須的配置

  • JwtDecoder:在oauth2ResourceServerConfigurer::init時,會構建JwtAuthenticationProvider,它就需要decorder以提供對"token"校驗解析。
  • JwtEncoder:雖然不是必須的,但我們自己系統登錄有令牌分發的需要。

3. 相關 Filter

  • BearerTokenAuthenticationFilter
    (雖沒繼承AbstractAuthenticationProcessingFilter但卻干着認證的事)
    首先通過DefaultBearerTokenResolver::resolve判斷是否含"token",然后構建BearerTokenAuthenticationToken並調用AuthenticationManager嘗試認證。
    將認證后的結果JwtAuthenticationToken設置到安全上下文中。如果中途出現了異常,則以該filter的authenticationEntryPoint(可通過.oauth2ResourceServer().authenticationEntryPoint配置) 處理。

4. 相關 Authentication

  1. BearerTokenAuthenticationToken
    代表原始token的一個過渡身份。
  2. JwtAuthenticationToken
    authenticated=true,進行實際系統訪問的身份。由BearerTokenAuthenticationToken認證后,通過JwtAuthenticationConverter轉換而來。

5. 相關 Provider

  • JwtAuthenticationProvider
    對 BearerTokenAuthenticationToken(帶access_token)進行認證。
    1. 內部會調用JwtDecoder::decode(可通過.bearerTokenResolver().jwt().decode配置)對 "token" 進行解析&驗證為Jwt對象。
    2. 調用JwtAuthenticationConverter(可通過.bearerTokenResolver().jwt().jwtAuthenticationConverter配置)嘗試對Jwt進一步轉換為進行實際系統訪問的(authenticated=true)JwtAuthenticationToken返回。(默認converter內部會調用jwtGrantedAuthoritiesConverter解析Jwt填充 authorities(將"scpoe"/"scp"聲明中空格分隔的字串轉為SimpleGrantedAuthority);將"sub"字段作為 principal)

五、前后端分離實戰定制

實際情況中,除了OAuth2登錄,我們系統自身也有完整的用戶體系,也有按自己業務定制的token構建分發服務。
三方登錄僅作為綁定手段,而且在初次三方登錄時 往往還需補全信息注冊到我們自己的用戶體系。

最終實現代碼以及效果展示都放在Github上了:spring-security-oauth2-sample

登錄流程,大致API流程:

六、結語

從上文也能看出,不得不提用SpringSecurity很多時候寧願遷出去自己寫套Configuer/Filter/Provider…,官方雖提供了很多服務,而且也能看出在盡可能定制化,但背后還是強制引入了太多邏輯,很難與實際業務契合,即便稍有不同在它基礎上定制也都需要付出很大代價。這代價不僅指新增代碼行數,為了定制或運行穩定 你首先就得徹底清楚它原本引入了哪些邏輯(這點官網文檔又做得不好),這就需要源碼閱讀和上手成本。

本文仍存在些許問題,特別是OAuth2AuthorizationCodeGrantFilter的重定向問題,還有與無狀態相悖的oauth2AuthorizedClientRepository涉及較少,也沒一張清晰流程圖,時間關系暫且就這樣了,有什么問題還希望指正討論。

關於 Spring Security 對 OAuth2 認證服務org.springframework.security:spring-security-oauth2-authorization-server的實現,以及前言提到的 SpringSecurity原理、JWT等等,后面有時間的話也會慢慢更。


免責聲明!

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



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