解决 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 社区所有即将举行的活动。

查看全部