Spring Security 6.4 中 RestClient 对 OAuth2 的支持

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

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

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

在 Spring Security 6.4 中,这一主题仍在继续,此次改进重点关注 RestClient,这是 Spring Framework 6.1 中引入的一种新的 HTTP 客户端。RestClient 提供了一个流畅的 API,与 WebClient 的 API 极其相似,但它是同步的,不依赖于响应式库。这意味着配置应用程序使用 OAuth2 Client 发起受保护资源请求变得更加简单,并且不需要任何额外的依赖。此外,还改进了使用 RestClient 的 Servlet 应用程序与使用 WebClient 的响应式应用程序之间的一致性,目标是使这两个技术栈采用通用的配置模型。

接下来,我们详细了解一下对 RestClient 的新支持以及 OAuth2 Client 的其他改进。

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'

应用程序需要通过使用 ClientRegistrationRepository bean 配置至少一个 ClientRegistrationClientRegistration 类是 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: http://localhost: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("http://localhost:8090/messages")
			.attributes(clientRegistrationId("messaging-client"))
			.retrieve()
			.body(Message[].class);

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

	public record Message(String message) {
	}

}

上述示例使用了静态方法,通过属性向拦截器提供 registrationId "messaging-client"。提供的值与之前 yaml 配置中的值匹配,Spring Security 就是通过这种方式知道获取访问令牌时应使用哪个客户端 id、secret、授权类型、scopes 以及其他信息。

当然,这只是一个示例,您不仅限于在端点中简单地返回结果。您可以在应用程序的任何部分执行此操作,例如负责发起受保护资源请求并将结果返回给应用程序的 @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 应用程序中对所有 OAuth2 Client 功能使用 RestClient 作为 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;
	}

}

注意:新支持中没有针对 password 授权类型的实现,因为对此授权类型的现有支持已被弃用,并计划在 Spring Security 7 中移除。

覆盖或省略默认参数

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 社区的所有近期活动。

查看全部