解決 Spring Security 中的 OAuth2 客戶端元件模型

工程 | Steve Riesenberg | 2023 年 8 月 22 日 | ...

在 Spring Security 5 中,隨著 OAuth2 Resource Server 和 OAuth2 Client 被引入框架,OAuth2 的發展取得了很大進展。

如今,利用 OAuth2 Resource Server 中提供的功能開發由 OAuth2 保護的應用非常方便。此外,我們可以利用 OAuth2 Client 的功能與 OAuth 2.0 和 OpenID Connect 1.0 提供商整合,從而可以透過 OAuth2 登入來認證使用者,和/或向由 OAuth2 保護的應用傳送受保護的請求。

然而,OAuth2 生態系統非常複雜,並且通常需要進行定製以與那些對各種 OAuth2 相關標準實現不靈活甚至不合規的第三方整合。考慮到所有這些複雜性,Spring Security 的 OAuth2 Client 元件在開發時就非常注重靈活性。這種靈活性伴隨著權衡,尤其是在配置方面。

我們聽取了社群關於配置的反饋意見,一個共同的主題是簡化各種 OAuth2 Client 元件的配置。讓我們看看在最新的 Spring Security 里程碑版本 6.2.0-M2 中配置是如何被簡化的。

更新: 參考文件的 OAuth2 頁面已更新,包含了 OAuth2 Client 的概述以及基於本文的示例。

入門

讓我們從 start.spring.io 上的一個簡單應用開始,我們可以以此為基礎構建各種可能遇到的用例。以下配置等同於 Spring Boot 提供的預設安排。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			.authorizeHttpRequests((authorize) -> authorize
				.anyRequest().authenticated()
			)
			.oauth2Client(Customizer.withDefaults())
			.oauth2Login(Customizer.withDefaults());

		return http.build();
	}

}

所需的一切僅僅是在 application.yml 中配置一個 ClientRegistration,如下所示:

spring:
  security:
    oauth2:
      client:
        registration:
          my-oauth2-client:
            provider: my-auth-server
            client-id: my-client-id
            client-secret: my-client-secret
            authorization-grant-type: authorization_code
            client-authentication-method: client_secret_basic
            scope: openid,profile,message.read,message.write
        provider:
          my-auth-server:
            issuer-uri: https://my-auth-server.com

用例

考慮到上述配置,讓我們思考以下用例:

用例:我想定製令牌請求引數

一個常見的用例是,在獲取 access_token 時需要定製請求引數。例如,假設我們想在令牌請求中新增一個自定義的 audience 引數,因為提供商要求 authorization_code 授權型別必須有此引數。

以前,我們必須使用 Spring Security DSL 確保此定製既適用於 OAuth2 登入(如果我們使用此功能),也適用於 OAuth2 Client 元件。配置可能看起來像這樣:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		OAuth2AuthorizationCodeGrantRequestEntityConverter requestEntityConverter =
			new OAuth2AuthorizationCodeGrantRequestEntityConverter();
		requestEntityConverter.addParametersConverter(parametersConverter());

		DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient =
			new DefaultAuthorizationCodeTokenResponseClient();
		accessTokenResponseClient.setRequestEntityConverter(requestEntityConverter);

		http
			.authorizeHttpRequests((authorize) -> authorize
				.anyRequest().authenticated()
			)
			.oauth2Client((oauth2Client) -> oauth2Client
				.authorizationCodeGrant((authorizationCode) -> authorizationCode
					.accessTokenResponseClient(accessTokenResponseClient)
				)
			)
			.oauth2Login((oauth2Login) -> oauth2Login
				.tokenEndpoint((tokenEndpoint) -> tokenEndpoint
					.accessTokenResponseClient(accessTokenResponseClient)
				)
			);

		return http.build();
	}

	private static Converter<OAuth2AuthorizationCodeGrantRequest, MultiValueMap<String, String>> parametersConverter() {
		return (grantRequest) -> {
			MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
			parameters.set("audience", "xyz_value");

			return parameters;
		};
	}

}

在最新的里程碑版本中,我們可以簡單地釋出一個型別為 OAuth2AccessTokenResponseClient<T> 的 bean(其中 TOAuth2AuthorizationCodeGrantRequest),它就會被自動檢測到。現在可以將此配置簡化為:

@Configuration
public class SecurityConfig {

	@Bean
	public DefaultAuthorizationCodeTokenResponseClient authorizationCodeAccessTokenResponseClient() {
		OAuth2AuthorizationCodeGrantRequestEntityConverter requestEntityConverter =
			new OAuth2AuthorizationCodeGrantRequestEntityConverter();
		requestEntityConverter.addParametersConverter(parametersConverter());

		DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient =
			new DefaultAuthorizationCodeTokenResponseClient();
		accessTokenResponseClient.setRequestEntityConverter(requestEntityConverter);

		return accessTokenResponseClient;
	}

	private static Converter<OAuth2AuthorizationCodeGrantRequest, MultiValueMap<String, String>> parametersConverter() {
		// ...
	}

}

注意: 請注意,由於這是我們進行的唯一定製,我們實際上可以完全省略 SecurityFilterChain bean,並使用 Spring Boot 提供的預設配置。如果我們需要配置其他內容,情況可能並非總是如此,但這仍然值得考慮,因為無論如何我們的配置都更簡單了。

對於其他授權型別,我們也可以釋出類似的 bean。例如,要定製 client_credentials 授權型別的令牌請求,我們可以釋出以下 bean:

@Configuration
public class SecurityConfig {

	@Bean
	public DefaultClientCredentialsTokenResponseClient clientCredentialsAccessTokenResponseClient() {
		OAuth2ClientCredentialsGrantRequestEntityConverter requestEntityConverter =
			new OAuth2ClientCredentialsGrantRequestEntityConverter();
		requestEntityConverter.addParametersConverter(parametersConverter());

		DefaultClientCredentialsTokenResponseClient accessTokenResponseClient =
				new DefaultClientCredentialsTokenResponseClient();
		accessTokenResponseClient.setRequestEntityConverter(requestEntityConverter);

		return accessTokenResponseClient;
	}

	private static Converter<OAuth2ClientCredentialsGrantRequest, MultiValueMap<String, String>> parametersConverter() {
		// ...
	}

}

用例:我想定製 OAuth2 Client 元件使用的 RestOperations

另一個常見的用例是,在獲取 access_token 時需要定製使用的 RestOperations(或響應式應用中的 WebClient)。我們可能需要這樣做來定製響應處理(透過自定義 HttpMessageConverter),或為企業網路應用代理設定(透過定製的 ClientHttpRequestFactory)。

假設我們想同時定製多種授權型別。以前,我們必須確保此定製既適用於 OAuth2 登入(如果我們使用此功能),也適用於 OAuth2 Client 元件。我們既要使用 Spring Security DSL(針對 authorization_code 授權型別),又要為其他授權型別釋出一個型別為 OAuth2AuthorizedClientManager 的 bean,這需要非常冗長的配置。配置可能看起來像這樣:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient =
			new DefaultAuthorizationCodeTokenResponseClient();
		accessTokenResponseClient.setRestOperations(restTemplate());

		http
			.authorizeHttpRequests((authorize) -> authorize
				.anyRequest().authenticated()
			)
			.oauth2Client((oauth2Client) -> oauth2Client
				.authorizationCodeGrant((authorizationCode) -> authorizationCode
					.accessTokenResponseClient(accessTokenResponseClient)
				)
			)
			.oauth2Login((oauth2Login) -> oauth2Login
				.tokenEndpoint((tokenEndpoint) -> tokenEndpoint
					.accessTokenResponseClient(accessTokenResponseClient)
				)
			);

		return http.build();
	}

	@Bean
	public OAuth2AuthorizedClientManager authorizedClientManager(
			ClientRegistrationRepository clientRegistrationRepository,
			OAuth2AuthorizedClientRepository authorizedClientRepository) {

		DefaultRefreshTokenTokenResponseClient refreshTokenAccessTokenResponseClient =
			new DefaultRefreshTokenTokenResponseClient();
		refreshTokenAccessTokenResponseClient.setRestOperations(restTemplate());

		DefaultClientCredentialsTokenResponseClient clientCredentialsAccessTokenResponseClient =
			new DefaultClientCredentialsTokenResponseClient();
		clientCredentialsAccessTokenResponseClient.setRestOperations(restTemplate());

		DefaultPasswordTokenResponseClient passwordAccessTokenResponseClient =
			new DefaultPasswordTokenResponseClient();
		passwordAccessTokenResponseClient.setRestOperations(restTemplate());

		OAuth2AuthorizedClientProvider authorizedClientProvider =
			OAuth2AuthorizedClientProviderBuilder.builder()
				.authorizationCode()
				.refreshToken((refreshToken) -> refreshToken
					.accessTokenResponseClient(refreshTokenAccessTokenResponseClient)
				)
				.clientCredentials((clientCredentials) -> clientCredentials
					.accessTokenResponseClient(clientCredentialsAccessTokenResponseClient)
				)
				.password((password) -> password
					.accessTokenResponseClient(passwordAccessTokenResponseClient)
				)
				.build();

		DefaultOAuth2AuthorizedClientManager authorizedClientManager =
			new DefaultOAuth2AuthorizedClientManager(
				clientRegistrationRepository, authorizedClientRepository);
		authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

		return authorizedClientManager;
	}

	@Bean
	public RestTemplate restTemplate() {
		// ...
	}

}

在最新的里程碑版本中,我們可以簡單地為每種 OAuth2AccessTokenResponseClient<T> 型別(其中 T 是 Spring Security 開箱即用支援的授權型別)釋出 bean。現在可以將此配置簡化為:

@Configuration
public class SecurityConfig {

	@Bean
	public DefaultAuthorizationCodeTokenResponseClient authorizationCodeAccessTokenResponseClient() {
		DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient =
			new DefaultAuthorizationCodeTokenResponseClient();
		accessTokenResponseClient.setRestOperations(restTemplate());

		return accessTokenResponseClient;
	}

	@Bean
	public DefaultRefreshTokenTokenResponseClient refreshTokenAccessTokenResponseClient() {
		DefaultRefreshTokenTokenResponseClient accessTokenResponseClient =
				new DefaultRefreshTokenTokenResponseClient();
		accessTokenResponseClient.setRestOperations(restTemplate());

		return accessTokenResponseClient;
	}

	@Bean
	public DefaultClientCredentialsTokenResponseClient clientCredentialsAccessTokenResponseClient() {
		DefaultClientCredentialsTokenResponseClient accessTokenResponseClient =
				new DefaultClientCredentialsTokenResponseClient();
		accessTokenResponseClient.setRestOperations(restTemplate());

		return accessTokenResponseClient;
	}

	@Bean
	public DefaultPasswordTokenResponseClient passwordAccessTokenResponseClient() {
		DefaultPasswordTokenResponseClient accessTokenResponseClient =
				new DefaultPasswordTokenResponseClient();
		accessTokenResponseClient.setRestOperations(restTemplate());

		return accessTokenResponseClient;
	}

	@Bean
	public RestTemplate restTemplate() {
		// ...
	}

}

實際上,我們甚至可以透過釋出相應的 OAuth2AccessTokenResponseClient bean 來選擇啟用擴充套件授權型別 jwt-bearer

@Bean
public DefaultJwtBearerTokenResponseClient jwtBearerAccessTokenResponseClient() {
	DefaultJwtBearerTokenResponseClient accessTokenResponseClient =
			new DefaultJwtBearerTokenResponseClient();
	accessTokenResponseClient.setRestOperations(restTemplate());

	return accessTokenResponseClient;
}

注意: 請注意,我們不需要釋出型別為 OAuth2AuthorizedClientManager 的 bean。現在 Spring Security 會為我們釋出一個。

現在我們可以透過依賴注入使用完全配置好的 OAuth2AuthorizedClientManager,例如這樣:

@RestController
class MyController {
	private final OAuth2AuthorizedClientManager authorizedClientManager;

	MyController(OAuth2AuthorizedClientManager authorizedClientManager) {
		this.authorizedClientManager = authorizedClientManager;
	}

	// ...
}

用例:我想啟用擴充套件授權型別

另一個用例涉及啟用和/或配置擴充套件授權型別。例如,Spring Security 支援 jwt-bearer 授權型別,但預設不啟用它。

以前,我們必須釋出一個型別為 OAuth2AuthorizedClientManager 的 bean,並確保同時重新啟用預設授權型別,這需要一些冗長的配置。配置可能看起來像這樣:

@Configuration
public class SecurityConfig {

	@Bean
	public OAuth2AuthorizedClientManager authorizedClientManager(
			ClientRegistrationRepository clientRegistrationRepository,
			OAuth2AuthorizedClientRepository authorizedClientRepository) {

		OAuth2AuthorizedClientProvider authorizedClientProvider =
			OAuth2AuthorizedClientProviderBuilder.builder()
				.authorizationCode()
				.refreshToken()
				.clientCredentials()
				.password()
				.provider(new JwtBearerOAuth2AuthorizedClientProvider())
				.build();

		DefaultOAuth2AuthorizedClientManager authorizedClientManager =
			new DefaultOAuth2AuthorizedClientManager(
				clientRegistrationRepository, authorizedClientRepository);
		authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

		return authorizedClientManager;
	}

}

在最新的里程碑版本中,我們可以簡單地釋出一個或多個 OAuth2AuthorizedClientProvider 的 bean,它們就會被自動檢測到。現在可以將此配置簡化為:

@Configuration
public class SecurityConfig {

	@Bean
	public OAuth2AuthorizedClientProvider jwtBearer() {
		return new JwtBearerOAuth2AuthorizedClientProvider();
	}

}

注意: 任何釋出的、非 Spring Security 提供的型別為 OAuth2AuthorizedClientProvider 的 bean 也將被檢測到,並在預設授權型別之後應用。

這還提供了定製現有授權型別、而無需重新定義預設配置的機會。例如,如果想定製 client_credentials 授權型別對應的 OAuth2AuthorizedClientProvider 的時鐘偏差(clock skew),我們可以簡單地釋出一個 bean,例如這樣:

@Configuration
public class SecurityConfig {

	@Bean
	public OAuth2AuthorizedClientProvider clientCredentials() {
		ClientCredentialsOAuth2AuthorizedClientProvider authorizedClientProvider =
				new ClientCredentialsOAuth2AuthorizedClientProvider();
		authorizedClientProvider.setClockSkew(Duration.ofMinutes(5));

		return authorizedClientProvider;
	}

}

結論

我希望您和我一樣對 Spring Security 中只需透過釋出 @Bean 即可簡化 OAuth2 Client 元件配置的方法感到興奮。如果您想參與其中,請嘗試使用這個里程碑版本並給我們反饋!我們將繼續傾聽並尋找機會,為 Spring Security 的使用者簡化配置。

訂閱 Spring 郵件列表

訂閱 Spring 郵件列表,保持聯絡

訂閱

先行一步

VMware 提供培訓和認證,助您加速進步。

瞭解更多

獲取支援

Tanzu Spring 透過一個簡單的訂閱,為 OpenJDK™、Spring 和 Apache Tomcat® 提供支援和二進位制檔案。

瞭解更多

即將舉行的活動

檢視 Spring 社群所有即將舉行的活動。

檢視全部