Spring Security 6.4 中 RestClient 对 OAuth2 的支持

工程 | Steve Riesenberg | 2024年10月28日 | ...

在 Spring Security 6.2 和 6.3 中,我们一直在努力改进使用 OAuth2 客户端的应用程序的配置。通过允许应用程序发布在应用程序启动期间自动包含在整体 OAuth2 客户端配置中的 Bean,简化了常见用例的配置。最近的改进包括

  • 只需发布类型为 OAuth2AuthorizedClientProvider(或 ReactiveOAuth2AuthorizedClientProvider)的 Bean,即可启用扩展授权类型。
  • 只需发布一个或多个类型为 OAuth2AccessTokenResponseClient(或 ReactiveOAuth2AccessTokenResponseClient)的 Bean,即可扩展 OAuth 2.0 访问令牌请求的自定义参数。
  • 如果尚未发布 OAuth2AuthorizedClientManager(或 ReactiveOAuth2AuthorizedClientManager)类型的 Bean,Spring Security 会自动发布一个,当应用程序需要获取访问令牌时,需要更少的样板配置。

在 Spring Security 6.4 中,此主题继续围绕 RestClient 进行改进,RestClient 是 Spring Framework 6.1 中引入的新 HTTP 客户端。RestClient 提供了一个流畅的 API,与 WebClient 的 API 非常相似,但它是同步的,并且不依赖于反应式库。这意味着配置应用程序以使用 OAuth2 客户端发出受保护资源请求要简单得多,并且不需要任何其他依赖项。此外,还进行了改进以提供使用 RestClient 的 servlet 应用程序和使用 WebClient 的反应式应用程序之间的一致性,目标是在一个通用配置模型上对齐这两个堆栈。

让我们详细检查 RestClient 的新支持以及 OAuth2 客户端的其他改进。

OAuth2 简要介绍

首先,让我们总结一下我们将要使用的 OAuth2 中的相关概念。

在 OAuth2 术语中,发出受保护资源请求意味着在发送到资源服务器的出站请求的 Authorization 标头中包含访问令牌。发起应用程序称为客户端,因为它会启动这些出站请求。目标应用程序称为资源服务器,因为它提供了一个 API 来访问属于资源所有者(例如用户)并受授权服务器保护的资源(例如数据)。授权服务器是一个负责创建和管理代表授权授予的访问令牌的系统,它会响应来自客户端代表资源所有者的请求(称为 OAuth 2.0 访问令牌请求)。

使用 RestClient 发出受保护资源请求

在进行了简要介绍之后,让我们看看如何在 Spring Security 6.4 中设置应用程序以使用 RestClient 发出受保护资源请求。前往 Spring Initializr 创建一个新的应用程序。如果您正在使用 Spring Boot 更新现有应用程序,则需要添加以下依赖项

implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

应用程序至少需要配置一个 ClientRegistration,方法是使用 ClientRegistrationRepository Bean。ClientRegistration 类是 Spring Security 中的域模型,其中包含特定 OAuth2 客户端的数据。每个客户端都必须在授权服务器上预先注册,此类包含从授权服务器获取的详细信息,例如 clientIdclientSecret。它还包含我们想要使用的 authorizationGrantType,例如 authorization_codeclient_credentials,以及可以根据需要选择性配置的几个其他参数。

以下示例使用 Spring Boot 配置属性配置了一个包含单个 ClientRegistrationInMemoryClientRegistrationRepository Bean

application.yml:

spring:
  security:
    oauth2:
      client:
        registration:
          messaging-client:
            provider: spring
            client-id: client1
            client-secret: my-secret
            authorization-grant-type: authorization_code
            scope: message.read,message.write
        provider:
          spring:
            issuer-uri: https://127.0.0.1:9000

上述配置允许 Spring Security 通过使用 本地授权服务器authorization_code 授权方式获取访问令牌。

Spring Security 提供了 OAuth2AuthorizedClientManager 的实现,它是一个可用于获取访问令牌(例如 JWT)的组件。Spring Security 会自动将此组件的一个实例发布为 Bean,这意味着我们只需将其注入到我们自己的配置中即可设置 RestClient,以便在我们的应用程序中发出受保护资源请求。以下示例配置了一个最小的 RestClient 并将其发布为 Bean

@Configuration
public class RestClientConfig {

	@Bean
	public RestClient restClient(RestClient.Builder builder, OAuth2AuthorizedClientManager authorizedClientManager) {
		OAuth2ClientHttpRequestInterceptor requestInterceptor =
			new OAuth2ClientHttpRequestInterceptor(authorizedClientManager);

		return builder.requestInterceptor(requestInterceptor).build();
	}

}

我们现在可以在自己的应用程序中发出受保护资源请求。以下示例演示了如何在 Spring MVC 控制器中执行此操作

import static org.springframework.security.oauth2.client.web.client.RequestAttributeClientRegistrationIdResolver.clientRegistrationId;

@RestController
public class MessagesController {

	private final RestClient restClient;

	public MessagesController(RestClient restClient) {
		this.restClient = restClient;
	}

	@GetMapping("/messages")
	public ResponseEntity<List<Message>> messages() {
		Message[] messages = this.restClient.get()
			.uri("https://127.0.0.1:8090/messages")
			.attributes(clientRegistrationId("messaging-client"))
			.retrieve()
			.body(Message[].class);

		return ResponseEntity.ok(Arrays.asList(messages));
	}

	public record Message(String message) {
	}

}

以上示例使用静态方法通过属性将 "messaging-client"registrationId 提供给拦截器。提供的 value 与之前提供的 yaml 配置中的 value 匹配,这是 Spring Security 如何能够知道在获取访问令牌时使用哪个客户端 ID、密钥、授权类型、范围和其他信息。

当然,这只是一个示例,您不仅限于在端点中返回结果。您可以在应用程序的任何所需部分执行此操作,例如负责发出受保护资源请求并将结果返回到应用程序的 @Service@Component

使用 RestClient 发出 OAuth 2.0 访问令牌请求

在 Spring Security 6.4 之前,servlet 堆栈的默认 HTTP 客户端是 RestTemplate。由于 RestTemplateWebClient 之间的 API 存在差异,因此使用 RestTemplate 自定义 servlet 应用程序的 OAuth 2.0 访问令牌请求与自定义使用 WebClient 的反应式应用程序的方式截然不同。

随着 Spring Framework 6.1 中引入 RestClient,现在可以通过分别利用 RestClientWebClient 作为每个堆栈的基础 HTTP 客户端,使这两个堆栈具有非常相似的配置模型。如果需要,可以使用 RestClient.create(RestTemplate)RestTemplate 创建 RestClient,从而为对齐 servlet 和反应式堆栈上的通用配置模型提供清晰的迁移路径,这是 Spring Security 7 的目标。

Spring Security 6.4 引入了 OAuth2AccessTokenResponseClient 的新实现来实现此目的。如果需要,可以选择在 servlet 应用程序中将 RestClient 作为所有 OAuth2 客户端功能的 HTTP 客户端。以下示例演示了使用自定义 RestClient 实例选择加入新支持的最小配置

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	private final RestClient restClient;

	@PostConstruct
	void initialize() {
		this.restClient = RestClient.builder()
			.messageConverters((messageConverters) -> {
				messageConverters.clear();
				messageConverters.add(new FormHttpMessageConverter());
				messageConverters.add(new OAuth2AccessTokenResponseHttpMessageConverter());
			})
			.defaultStatusHandler(new OAuth2ErrorResponseErrorHandler())
			// TODO: Customize the instance of RestClient as needed...
			.build();
	}

	@Bean
	public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> authorizationCodeAccessTokenResponseClient() {
		RestClientAuthorizationCodeTokenResponseClient accessTokenResponseClient =
			new RestClientAuthorizationCodeTokenResponseClient();
		accessTokenResponseClient.setRestClient(this.restClient);

		return accessTokenResponseClient;
	}

	@Bean
	public OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenAccessTokenResponseClient() {
		RestClientRefreshTokenTokenResponseClient accessTokenResponseClient =
			new RestClientRefreshTokenTokenResponseClient();
		accessTokenResponseClient.setRestClient(this.restClient);

		return accessTokenResponseClient;
	}

	@Bean
	public OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsAccessTokenResponseClient() {
		RestClientClientCredentialsTokenResponseClient accessTokenResponseClient =
			new RestClientClientCredentialsTokenResponseClient();
		accessTokenResponseClient.setRestClient(this.restClient);

		return accessTokenResponseClient;
	}

	@Bean
	public OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> passwordAccessTokenResponseClient() {
		return (grantRequest) -> {
			throw new UnsupportedOperationException("The `password` grant type is not supported.");
		};
	}

	@Bean
	public OAuth2AccessTokenResponseClient<JwtBearerGrantRequest> jwtBearerAccessTokenResponseClient() {
		RestClientJwtBearerTokenResponseClient accessTokenResponseClient =
			new RestClientJwtBearerTokenResponseClient();
		accessTokenResponseClient.setRestClient(this.restClient);

		return accessTokenResponseClient;
	}

	@Bean
	public OAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> tokenExchangeAccessTokenResponseClient() {
		RestClientTokenExchangeTokenResponseClient accessTokenResponseClient =
			new RestClientTokenExchangeTokenResponseClient();
		accessTokenResponseClient.setRestClient(this.restClient);

		return accessTokenResponseClient;
	}

}

注意:由于对这种授权类型的现有支持已弃用,并计划在 Spring Security 7 中删除,因此新支持中没有 password 授权类型的实现。

覆盖或省略默认参数

Spring Security 通过 OAuth2AccessTokenResponseClient(或 ReactiveOAuth2AccessTokenResponseClient)接口的实现支持多种授权类型。一个常见需求是能够自定义 OAuth 2.0 访问令牌请求的参数,这在授权服务器有特定要求或提供不受支持规范涵盖的功能时很常见。

在 Spring Security 6.3 及更早版本中,反应式应用程序无法覆盖或省略 Spring Security 设置的参数值,需要使用解决方法来自定义应用程序以满足此类用例。现在可以通过 setParametersConverter() 自定义钩子为反应式应用程序(使用 WebClient)和 servlet 应用程序(使用 RestClient)覆盖参数。在这种情况下,需要注意的是,所有授权类型特定的和默认参数将首先设置。自定义 parametersConverter 提供的任何参数都将覆盖现有参数。

除了覆盖参数外,现在还可以省略授权服务器可能拒绝的参数。例如,当ClientRegistration#clientAuthenticationMethod设置为private_key_jwt时,我们可以使用包含生成的 JWT 的客户端断言提供客户端身份验证。某些授权服务器可能会选择拒绝同时包含client_idclient_assertion参数的请求。在这种情况下,因为client_id是 Spring Security 提供的默认参数,所以我们需要一种方法根据我们将使用客户端断言提供客户端身份验证的知识来省略此参数。

Spring Security 6.4 提供了使用setParametersCustomizer()自定义钩子省略 OAuth 2.0 访问令牌请求参数的功能。以下示例显示了在使用客户端断言进行client_credentials授权类型的客户端身份验证时如何省略client_id参数。

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

	@Bean
	public ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsAccessTokenResponseClient() {
		WebClientReactiveClientCredentialsTokenResponseClient accessTokenResponseClient =
			new WebClientReactiveClientCredentialsTokenResponseClient();
		accessTokenResponseClient.addParametersConverter(
			new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver()));
		accessTokenResponseClient.setParametersCustomizer((parameters) -> {
			if (parameters.containsKey(OAuth2ParameterNames.CLIENT_ASSERTION)) {
				parameters.remove(OAuth2ParameterNames.CLIENT_ID);
			}
		});

		return accessTokenResponseClient;
	}

	private Function<ClientRegistration, JWK> jwkResolver() {
		// ...
	}

}

提示:当使用RestClientClientCredentialsTokenResponseClient(或其他授权类型的替代实现)时,您也可以为 Servlet 应用程序提供等效的配置。

结论

Spring Security 6.4 是一个令人兴奋的版本,包含了对使用 OAuth2 保护的应用程序的许多改进,以及许多其他令人兴奋的功能。在这篇文章中,我们检查了即将发布版本中的三个新功能。首先,我们讨论了如何在非反应式应用程序中使用RestClient发出受保护的资源请求,而无需额外的依赖项。接下来,我们研究了如何在所有地方选择使用RestClient,并享受与反应式栈一致的简化和更一致的配置。最后,我们学习了如何在 OAuth 2.0 访问令牌请求中覆盖或省略默认参数,这解锁了以前难以处理的高级场景。

我希望您和我一样对这一轮改进以及 Spring Security 6.4 中提供的其他所有功能感到兴奋。这些功能以及更多功能在 Spring Security 6.4.0-RC1 中可供预发布,所以请尝试一下。我们很乐意听取您的反馈!

获取 Spring 新闻

与 Spring 新闻保持联系

订阅

领先一步

VMware 提供培训和认证,以加速您的进步。

了解更多

获取支持

Tanzu Spring 在一个简单的订阅中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区中所有即将举行的活动。

查看全部