處理 Spring Security 中的 OAuth2 Client 元件模型

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

在 Spring Security 5 中,我們看到了 OAuth2 相關功能的許多進展,像是將 OAuth2 Resource Server 和 OAuth2 Client 引入框架中。

現在,使用 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 授權。

先前,我們必須確保此自訂同時應用於 OAuth2 登入(如果我們正在使用此功能)和使用 Spring Security DSL 的 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),或者為公司網路套用 Proxy 設定(透過客製化的 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> 發布 bean(其中 T 是 Spring Security 中開箱即用的授權類型)。 此配置現在可以簡化為

@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 的時鐘偏差,我們可以簡單地發布一個如下所示的 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 社群所有即將到來的活動。

檢視全部