Spring Security 6.4 中 RestClient 对 OAuth2 的支持

工程 | 史蒂夫·里森伯格 | 2024年10月28日 | ...

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

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

在 Spring Security 6.4 中,这一主题继续以一系列改进为主,重点关注 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'

应用程序需要至少一个通过 ClientRegistrationRepository bean 配置的 ClientRegistrationClientRegistration 类是 Spring Security 中的域模型,其中包含特定 OAuth2 客户端的数据。每个客户端都必须在授权服务器上预注册,并且此​​类包含从授权服务器获取的详细信息,例如 clientIdclientSecret。它还包含我们希望使用的 authorizationGrantType,例如 authorization_codeclient_credentials,以及根据需要可选配置的几个附加参数。

以下示例使用 Spring Boot 配置属性配置了一个 InMemoryClientRegistrationRepository bean,其中包含一个 ClientRegistration

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://: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://:8090/messages")
			.attributes(clientRegistrationId("messaging-client"))
			.retrieve()
			.body(Message[].class);

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

	public record Message(String message) {
	}

}

上面的示例使用静态方法通过属性向拦截器提供 "messaging-client"registrationId。提供的值与前面提供的 yaml 配置中的值匹配,这就是 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;
	}

}

注意:新支持中没有 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_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 社区所有即将举行的活动。

查看所有