Spring?Security?OAuth?Client配置加載源碼解析
引言
這一節(jié)我們以前面默認(rèn)的OAuth2 客戶端集成為例,來了解下配置文件的加載,示例見第二、第三節(jié)。
InMemoryClientRegistrationRepository
假如你沒有看過相關(guān)視頻,或者書,但想要自己分析源碼,應(yīng)該怎么分析?
在分析原理之前,我們一定要找到突破口,否則就會無從下手,突破口就是之前集成Gitee OAuth的配置文件,我們分析任何框架的源碼都是如此,從表象到骨髓,一層層深入。
spring:
security:
oauth2:
client:
registration:
gitee:
client-id: gitee-client-id
client-secret: gitee-client-secret
authorization-grant-type: authorization_code
redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}'
client-name: Gitee
github:
client-id: b4713d47174917b34c28
client-secret: 898389369c2e9f3d1d0ff4543ba1d9b45adfd093
provider:
gitee:
authorization-uri: https://gitee.com/oauth/authorize
token-uri: https://gitee.com/oauth/token
user-info-uri: https://gitee.com/api/v5/user
user-name-attribute: name我們點進去,內(nèi)部就是一個 OAuth2ClientProperties 類,這個類配置了 @ConfigurationProperties 注解用來加載配置文件,用IDE查找一下該類用在了哪些地方,出來很多類,在這種沒法一下判斷的情況下,我的辦法就是一個個進去看,判斷哪個類最有可能,Reactive開頭的類都是在響應(yīng)式環(huán)境下使用的,都可以忽略。

這里 OAuth2ClientRegistrationRepositoryConfiguration 就是我們要找的類,在該類中會加載一個 InMemoryClientRegistrationRepository Bean,該Bean用于本地存儲客戶端注冊信息的。
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(OAuth2ClientProperties.class)
@Conditional(ClientsConfiguredCondition.class)
class OAuth2ClientRegistrationRepositoryConfiguration {
@Bean
@ConditionalOnMissingBean(ClientRegistrationRepository.class)
InMemoryClientRegistrationRepository clientRegistrationRepository(OAuth2ClientProperties properties) {
List<ClientRegistration> registrations = new ArrayList<>(
OAuth2ClientPropertiesRegistrationAdapter.getClientRegistrations(properties).values());
return new InMemoryClientRegistrationRepository(registrations);
}
}配置
這里有如下幾個配置:
- @Configuration(proxyBeanMethods = false):使用@Bean時候的配置,proxyBeanMethods表示是否使用代理來獲取bean,這里表示不使用代理獲取,這樣配置能夠提高Spring 的加載速度。
- @EnableConfigurationProperties:開啟
OAuth2ClientPropertiesSpring Bean - @Conditional(ClientsConfiguredCondition.class): 只有在存在
ClientsConfiguredConditionBean的時候,才注冊該類
InMemoryClientRegistrationRepository Bean只有在 ClientRegistrationRepository 不存在的時候才會加載。
該Bean的流程是從 OAuth2ClientProperties 配置中獲取OAuth客戶端信息,構(gòu)建 ClientRegistration 對象,并存儲在 InMemoryClientRegistrationRepository 中。
這個類看似好像到這里就完了,線索斷了嗎,其實沒有,OAuth客戶端配置的加載確實是完成了,那后面其他類肯定會使用到該配置類,這個后面在看,別忘記我們的問題。
回到 OAuth2ClientRegistrationRepositoryConfiguration 所在的目錄,你會發(fā)現(xiàn)該目錄下還有兩個文件 OAuth2ClientAutoConfiguration 和 OAuth2WebSecurityConfiguration ,
看下 OAuth2ClientAutoConfiguration 類,原來 OAuth2ClientRegistrationRepositoryConfiguration 也是由它引導(dǎo)加載的,那么我們看下另外一個類。
OAuth2WebSecurityConfiguration
OAuth2WebSecurityConfiguration 類中,注冊了 InMemoryOAuth2AuthorizedClientService 、 OAuth2AuthorizedClientRepository 、 SecurityFilterChain 。
(1) InMemoryOAuth2AuthorizedClientService 是OAuth2AuthorizedClientService的實現(xiàn),用于本地保存OAuth2授權(quán)客戶端,具有保存已認(rèn)證的授權(quán)客戶端(saveAuthorizedClient)、移除已認(rèn)證的授權(quán)客戶端(removeAuthorizedClient)和獲取已認(rèn)證的授權(quán)客戶端(loadAuthorizedClient)3個功能。
在該類中,你會發(fā)現(xiàn)保存了ClientRegistrationRepository對象,并且loadAuthorizedClient 和 removeAuthorizedClient 的時候,都會調(diào)用ClientRegistrationRepository中的findByRegistrationId方法,至此又跟前面加載的InMemoryClientRegistrationRepository聯(lián)系在了一起。
(2) AuthenticatedPrincipalOAuth2AuthorizedClientRepository 是OAuth2AuthorizedClientRepository的實現(xiàn),用于維護 principal 主體(理解為已認(rèn)證的用戶)與授權(quán)客戶端OAuth2AuthorizedClient的關(guān)系,并且提供了一個匿名的處理,如果是匿名使用HttpSessionOAuth2AuthorizedClientRepository處理(也可覆蓋提供)。
該類提供了loadAuthorizedClient、saveAuthorizedClient、removeAuthorizedClient、setAnonymousAuthorizedClientRepository 幾個公開方法
(3) SecurityFilterChain :一個過濾器鏈,用來匹配請求,匹配的請求將執(zhí)行一系列過濾器。
@Bean
SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests((requests) -> requests.anyRequest().authenticated()); http.oauth2Login(Customizer.withDefaults()); http.oauth2Client(); return http.build(); }代碼可見,內(nèi)部使用HttpSecurity構(gòu)建了一個默認(rèn)的SecurityFilterChain,表明任何請求都可以使用該過濾器鏈,使用oauth2提供的默認(rèn)登錄方式(提供一個/login的默認(rèn)登錄頁面),再最后http.build()用于構(gòu)建一個SecurityFilterChain,看看此處代碼。
http.build()
build()位于HttpSecurity的父類AbstractSecurityBuilder
public final O build() throws Exception {
if (this.building.compareAndSet(false, true)) {
this.object = doBuild();
return this.object;
}
throw new AlreadyBuiltException("This object has already been built");
}build()使用了CAS來保證構(gòu)建的對象只會構(gòu)建一次,我們主要看doBuild(),其是一個抽象方法,用于子類去實現(xiàn)具體的構(gòu)建邏輯,該子類是AbstractConfiguredSecurityBuilder。
protected final O doBuild() throws Exception {
synchronized (this.configurers) {
//標(biāo)記構(gòu)建狀態(tài)
this.buildState = BuildState.INITIALIZING;
//加載配置前的處理,默認(rèn)空實現(xiàn),子類可以覆蓋實現(xiàn)
beforeInit();
//加載配置
init();
//修改構(gòu)建狀態(tài)
this.buildState = BuildState.CONFIGURING;
//在開始配置之前的處理
beforeConfigure();
//開始配置,調(diào)用實現(xiàn)了SecurityConfigurer的configure()
//在這里會將各種內(nèi)置的過濾器添加到HttpSecurity中
configure();
this.buildState = BuildState.BUILDING;
//開始構(gòu)建要返回的對象,抽象返回,子類實現(xiàn)構(gòu)建邏輯
O result = performBuild();
this.buildState = BuildState.BUILT;
return result;
}
}HttpSecurity的構(gòu)建邏輯
protected DefaultSecurityFilterChain performBuild() {
ExpressionUrlAuthorizationConfigurer<?> expressionConfigurer = getConfigurer(
ExpressionUrlAuthorizationConfigurer.class);
AuthorizeHttpRequestsConfigurer<?> httpConfigurer = getConfigurer(AuthorizeHttpRequestsConfigurer.class);
boolean oneConfigurerPresent = expressionConfigurer == null ^ httpConfigurer == null;
Assert.state((expressionConfigurer == null && httpConfigurer == null) || oneConfigurerPresent,
"authorizeHttpRequests cannot be used in conjunction with authorizeRequests. Please select just one.");
this.filters.sort(OrderComparator.INSTANCE);
List<Filter> sortedFilters = new ArrayList<>(this.filters.size());
for (Filter filter : this.filters) {
sortedFilters.add(((OrderedFilter) filter).filter);
}
return new DefaultSecurityFilterChain(this.requestMatcher, sortedFilters);
}此處先判斷是否同時加載了ExpressionUrlAuthorizationConfigurer(基于SpEL的URL授權(quán))和AuthorizeHttpRequestsConfigurer(使用AuthorizationManager添加基于 URL 的授權(quán),該類是5.5新增),這兩個不能同時使用。
然后再對加載的過濾器進行Order排序,最后生成DefaultSecurityFilterChain對象返回。
我們可以看下此處filters的值,發(fā)現(xiàn)已經(jīng)加載了18個filter,如下,其中OAuth2開頭的幾個過濾器特別顯眼。
DisableEncodeUrlFilter
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CsrfFilter
LogoutFilter
OAuth2AuthorizationRequestRedirectFilter
OAuth2AuthorizationRequestRedirectFilter
OAuth2LoginAuthenticationFilter
DefaultLoginPageGeneratingFilter
DefaultLogoutPageGeneratingFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
AnonymousAuthenticationFilter
OAuth2AuthorizationCodeGrantFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
到這里,Spring Security OAuth2 的默認(rèn)配置已經(jīng)加載完了,這里描述內(nèi)容只是我們表象能看到的,其實還有其他的內(nèi)容,比如HttpSecurity等還有很多。
后面我們將深入分析這18個過濾器都干了哪些事。
要學(xué)習(xí)一個新框架,我一般會按照如下步驟來實施:
(1)根據(jù)官方文檔搭建Demo,先跑起來,有一個整體觀
(2)分析源碼,從Demo的功能配置入手,找到突破口
(3)每次分析源碼,要帶著問題看
(4)根據(jù)源碼分析出來的思路,畫架構(gòu)圖、流程圖
(5)學(xué)習(xí)框架的實現(xiàn)思路,取其精華去其糟粕
以上就是Spring Security OAuth Client配置加載源碼解析的詳細(xì)內(nèi)容,更多關(guān)于Spring Security OAuth Client配置加載的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
TransmittableThreadLocal通過javaAgent實現(xiàn)線程傳遞并支持ForkJoin
mybatis插件pageHelper實現(xiàn)分頁效果
Android應(yīng)用開發(fā)的一般文件組織結(jié)構(gòu)講解

