OAuth2 WebFlux

Spring Security 提供全面的 OAuth 2.0 支持。本节讨论如何将 OAuth 2.0 集成到您的响应式应用程序中。

概述

Spring Security 的 OAuth 2.0 支持包含两个主要功能集

OAuth2 登录 是一款非常强大的 OAuth2 客户端功能,在参考文档中值得拥有自己的部分。但是,它并非独立的功能,需要 OAuth2 客户端才能发挥作用。

这些功能集涵盖了 OAuth 2.0 授权框架 中定义的资源服务器客户端角色,而授权服务器角色则由 Spring Authorization Server 涵盖,这是一个基于 Spring Security 的独立项目。

OAuth2 中的资源服务器客户端角色通常由一个或多个服务器端应用程序表示。此外,授权服务器角色可以由一个或多个第三方(当在组织内集中身份管理和/或身份验证时)应用程序(当使用 Spring Authorization Server 时)表示。

例如,一个典型的基于 OAuth2 的微服务架构可能包含一个面向用户的客户端应用程序、几个提供 REST API 的后端资源服务器以及一个用于管理用户和身份验证问题的第三方授权服务器。拥有仅表示这些角色之一的单个应用程序并需要与提供其他角色的一个或多个第三方集成也很常见。

Spring Security 处理这些场景以及更多场景。以下部分涵盖了 Spring Security 提供的角色,并包含常见场景的示例。

OAuth2 资源服务器

本节包含 OAuth2 资源服务器功能的摘要以及示例。有关完整的参考文档,请参阅 OAuth 2.0 资源服务器

要开始,请将 spring-security-oauth2-resource-server 依赖项添加到您的项目中。当使用 Spring Boot 时,请添加以下启动器

使用 Spring Boot 的 OAuth2 客户端
  • Gradle

  • Maven

implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

有关不使用 Spring Boot 时的其他选项,请参阅 获取 Spring Security

考虑以下 OAuth2 资源服务器的使用案例

使用 OAuth2 访问令牌保护访问

使用 OAuth2 访问令牌保护对 API 的访问非常常见。在大多数情况下,Spring Security 仅需要最少的配置即可使用 OAuth2 来保护应用程序。

Spring Security 支持两种类型的 Bearer 令牌,每种令牌都使用不同的组件进行验证

  • JWT 支持 使用 ReactiveJwtDecoder bean 来验证签名和解码令牌

  • 不透明令牌支持 使用 ReactiveOpaqueTokenIntrospector bean 来内省令牌

JWT 支持

以下示例使用 Spring Boot 配置属性配置 ReactiveJwtDecoder bean

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://my-auth-server.com

当使用 Spring Boot 时,这就是所有需要的。Spring Boot 提供的默认安排等效于以下内容

使用 JWT 配置资源服务器
  • Java

  • Kotlin

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

	@Bean
	public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
		http
			.authorizeExchange((authorize) -> authorize
				.anyExchange().authenticated()
			)
			.oauth2ResourceServer((oauth2) -> oauth2
				.jwt(Customizer.withDefaults())
			);
		return http.build();
	}

	@Bean
	public ReactiveJwtDecoder jwtDecoder() {
		return ReactiveJwtDecoders.fromIssuerLocation("https://my-auth-server.com");
	}

}
import org.springframework.security.config.web.server.invoke

@Configuration
@EnableWebFluxSecurity
class SecurityConfig {

	@Bean
	fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
		return http {
			authorizeExchange {
				authorize(anyExchange, authenticated)
			}
			oauth2ResourceServer {
				jwt { }
			}
		}
	}

	@Bean
	fun jwtDecoder(): ReactiveJwtDecoder {
		return ReactiveJwtDecoders.fromIssuerLocation("https://my-auth-server.com")
	}

}

不透明令牌支持

以下示例使用 Spring Boot 配置属性配置 OpaqueTokenIntrospector bean

spring:
  security:
    oauth2:
      resourceserver:
        opaquetoken:
          introspection-uri: https://my-auth-server.com/oauth2/introspect
          client-id: my-client-id
          client-secret: my-client-secret

当使用 Spring Boot 时,这就是所有需要的。Spring Boot 提供的默认安排等效于以下内容

使用不透明令牌配置资源服务器
  • Java

  • Kotlin

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

	@Bean
	public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
		http
			.authorizeExchange((authorize) -> authorize
				.anyExchange().authenticated()
			)
			.oauth2ResourceServer((oauth2) -> oauth2
				.opaqueToken(Customizer.withDefaults())
			);
		return http.build();
	}

	@Bean
	public ReactiveOpaqueTokenIntrospector opaqueTokenIntrospector() {
		return new SpringReactiveOpaqueTokenIntrospector(
			"https://my-auth-server.com/oauth2/introspect", "my-client-id", "my-client-secret");
	}

}
import org.springframework.security.config.web.server.invoke

@Configuration
@EnableWebFluxSecurity
class SecurityConfig {

	@Bean
	fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
		return http {
			authorizeExchange {
				authorize(anyExchange, authenticated)
			}
			oauth2ResourceServer {
				opaqueToken { }
			}
		}
	}

	@Bean
	fun opaqueTokenIntrospector(): ReactiveOpaqueTokenIntrospector {
		return SpringReactiveOpaqueTokenIntrospector(
			"https://my-auth-server.com/oauth2/introspect", "my-client-id", "my-client-secret"
		)
	}

}

使用自定义 JWT 保护访问

使用 JWT 保护 API 访问权限是一个相当常见的目标,尤其是在前端开发为单页面应用程序时。Spring Security 中的 OAuth2 资源服务器支持可以用于任何类型的 Bearer 令牌,包括自定义 JWT。

使用 JWT 保护 API 所需的全部只是一个 ReactiveJwtDecoder bean,它用于验证签名和解码令牌。Spring Security 将自动使用提供的 bean 在 SecurityWebFilterChain 中配置保护。

以下示例使用 Spring Boot 配置属性配置 ReactiveJwtDecoder bean

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          public-key-location: classpath:my-public-key.pub

您可以将公钥作为类路径资源提供(在本例中称为 my-public-key.pub)。

当使用 Spring Boot 时,这就是所有需要的。Spring Boot 提供的默认安排等效于以下内容

使用自定义 JWT 配置资源服务器
  • Java

  • Kotlin

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

	@Bean
	public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
		http
			.authorizeExchange((authorize) -> authorize
				.anyExchange().authenticated()
			)
			.oauth2ResourceServer((oauth2) -> oauth2
				.jwt(Customizer.withDefaults())
			);
		return http.build();
	}

	@Bean
	public ReactiveJwtDecoder jwtDecoder() {
		return NimbusReactiveJwtDecoder.withPublicKey(publicKey()).build();
	}

	private RSAPublicKey publicKey() {
		// ...
	}

}
import org.springframework.security.config.web.server.invoke

@Configuration
@EnableWebFluxSecurity
class SecurityConfig {

	@Bean
	fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
		return http {
			authorizeExchange {
				authorize(anyExchange, authenticated)
			}
			oauth2ResourceServer {
				jwt { }
			}
		}
	}

	@Bean
	fun jwtDecoder(): ReactiveJwtDecoder {
		return NimbusReactiveJwtDecoder.withPublicKey(publicKey()).build()
	}

	private fun publicKey(): RSAPublicKey {
		// ...
	}

}

Spring Security 没有提供用于铸造令牌的端点。但是,Spring Security 确实提供了 JwtEncoder 接口以及一个实现,即 NimbusJwtEncoder

OAuth2 客户端

本节包含 OAuth2 客户端功能的摘要以及示例。有关完整的参考文档,请参阅 OAuth 2.0 客户端OAuth 2.0 登录

要开始使用,请将 spring-security-oauth2-client 依赖项添加到您的项目中。在使用 Spring Boot 时,请添加以下启动器

使用 Spring Boot 的 OAuth2 客户端
  • Gradle

  • Maven

implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

有关不使用 Spring Boot 时的其他选项,请参阅 获取 Spring Security

考虑以下 OAuth2 客户端的使用场景

使用 OAuth2 登录用户

通常需要用户通过 OAuth2 登录。 OpenID Connect 1.0 提供了一种名为 id_token 的特殊令牌,旨在为 OAuth2 客户端提供执行用户身份验证和登录用户的功能。在某些情况下,OAuth2 可以直接用于登录用户(例如,流行的社交登录提供商(如 GitHub 和 Facebook)未实现 OpenID Connect 的情况)。

以下示例配置应用程序以充当能够使用 OAuth2 或 OpenID Connect 登录用户的 OAuth2 客户端

配置 OAuth2 登录
  • Java

  • Kotlin

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

	@Bean
	public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
		http
			// ...
			.oauth2Login(Customizer.withDefaults());
		return http.build();
	}

}
import org.springframework.security.config.web.server.invoke

@Configuration
@EnableWebFluxSecurity
class SecurityConfig {

	@Bean
	fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
		return http {
			// ...
			oauth2Login { }
		}
	}

}

除了上述配置之外,应用程序还需要至少配置一个 ClientRegistration,方法是使用 ReactiveClientRegistrationRepository bean。以下示例使用 Spring Boot 配置属性配置 InMemoryReactiveClientRegistrationRepository bean

spring:
  security:
    oauth2:
      client:
        registration:
          my-oidc-client:
            provider: my-oidc-provider
            client-id: my-client-id
            client-secret: my-client-secret
            authorization-grant-type: authorization_code
            scope: openid,profile
        provider:
          my-oidc-provider:
            issuer-uri: https://my-oidc-provider.com

通过以上配置,应用程序现在支持两个额外的端点

  1. 登录端点(例如 /oauth2/authorization/my-oidc-client)用于启动登录并执行重定向到第三方授权服务器。

  2. 重定向端点(例如 /login/oauth2/code/my-oidc-client)由授权服务器用于重定向回客户端应用程序,并将包含一个 code 参数,用于通过访问令牌请求获取 id_token 和/或 access_token

上述配置中 openid 范围的存在表示应使用 OpenID Connect 1.0。这指示 Spring Security 在请求处理期间使用特定于 OIDC 的组件(例如 OidcReactiveOAuth2UserService)。如果没有此范围,Spring Security 将改为使用特定于 OAuth2 的组件(例如 DefaultReactiveOAuth2UserService)。

访问受保护的资源

对受 OAuth2 保护的第三方 API 发出请求是 OAuth2 客户端的核心用例。这是通过授权客户端(在 Spring Security 中由 OAuth2AuthorizedClient 类表示)并在出站请求的 Authorization 标头中放置 Bearer 令牌来访问受保护的资源来实现的。

以下示例配置应用程序以充当能够从第三方 API 请求受保护资源的 OAuth2 客户端

配置 OAuth2 客户端
  • Java

  • Kotlin

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

	@Bean
	public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
		http
			// ...
			.oauth2Client(Customizer.withDefaults());
		return http.build();
	}

}
import org.springframework.security.config.web.server.invoke

@Configuration
@EnableWebFluxSecurity
class SecurityConfig {

	@Bean
	fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
		return http {
			// ...
			oauth2Client { }
		}
	}

}

以上示例没有提供登录用户的方法。您可以使用任何其他登录机制(例如 formLogin())。请参阅下一节以获取将 oauth2Client()oauth2Login() 结合使用的示例。

除了上述配置之外,应用程序还需要至少配置一个 ClientRegistration,方法是使用 ReactiveClientRegistrationRepository bean。以下示例使用 Spring Boot 配置属性配置 InMemoryReactiveClientRegistrationRepository bean

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
            scope: message.read,message.write
        provider:
          my-auth-server:
            issuer-uri: https://my-auth-server.com

除了配置 Spring Security 以支持 OAuth2 客户端功能外,您还需要确定将如何访问受保护的资源并相应地配置您的应用程序。Spring Security 提供了 ReactiveOAuth2AuthorizedClientManager 的实现,用于获取可用于访问受保护资源的访问令牌。

当不存在 ReactiveOAuth2AuthorizedClientManager 时,Spring Security 会为您注册一个默认的 ReactiveOAuth2AuthorizedClientManager bean。

使用 ReactiveOAuth2AuthorizedClientManager 的最简单方法是通过一个 ExchangeFilterFunction,该函数通过 WebClient 拦截请求。

以下示例使用默认的 ReactiveOAuth2AuthorizedClientManager 配置一个 WebClient,该 WebClient 能够通过在每个请求的 Authorization 标头中放置 Bearer 令牌来访问受保护的资源

使用 ExchangeFilterFunction 配置 WebClient
  • Java

  • Kotlin

@Configuration
public class WebClientConfig {

	@Bean
	public WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
		ServerOAuth2AuthorizedClientExchangeFilterFunction filter =
				new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
		return WebClient.builder()
				.filter(filter)
				.build();
	}

}
@Configuration
class WebClientConfig {

	@Bean
	fun webClient(authorizedClientManager: ReactiveOAuth2AuthorizedClientManager): WebClient {
		val filter = ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager)
		return WebClient.builder()
			.filter(filter)
			.build()
	}

}

此配置的 WebClient 可以像以下示例中那样使用

使用 WebClient 访问受保护的资源
  • Java

  • Kotlin

import static org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId;

@RestController
public class MessagesController {

	private final WebClient webClient;

	public MessagesController(WebClient webClient) {
		this.webClient = webClient;
	}

	@GetMapping("/messages")
	public Mono<ResponseEntity<List<Message>>> messages() {
		return this.webClient.get()
				.uri("https://127.0.0.1:8090/messages")
				.attributes(clientRegistrationId("my-oauth2-client"))
				.retrieve()
				.toEntityList(Message.class);
	}

	public record Message(String message) {
	}

}
import org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId

@RestController
class MessagesController(private val webClient: WebClient) {

	@GetMapping("/messages")
	fun messages(): Mono<ResponseEntity<List<Message>>> {
		return webClient.get()
			.uri("https://127.0.0.1:8090/messages")
			.attributes(clientRegistrationId("my-oauth2-client"))
			.retrieve()
			.toEntityList<Message>()
	}

	data class Message(val message: String)

}

访问当前用户的受保护资源

当用户通过 OAuth2 或 OpenID Connect 登录时,授权服务器可能会提供一个访问令牌,该令牌可以直接用于访问受保护的资源。这很方便,因为它只需要为这两种用例同时配置一个 ClientRegistration

本节将使用 OAuth2 登录用户访问受保护的资源组合到一个配置中。还存在其他高级场景,例如为登录配置一个 ClientRegistration,为访问受保护的资源配置另一个 ClientRegistration。所有此类场景都将使用相同的基本配置。

以下示例配置应用程序以充当能够登录用户 *和* 从第三方 API 请求受保护资源的 OAuth2 客户端

配置 OAuth2 登录和 OAuth2 客户端
  • Java

  • Kotlin

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

	@Bean
	public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
		http
			// ...
			.oauth2Login(Customizer.withDefaults())
			.oauth2Client(Customizer.withDefaults());
		return http.build();
	}

}
import org.springframework.security.config.web.server.invoke

@Configuration
@EnableWebFluxSecurity
class SecurityConfig {

	@Bean
	fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
		return http {
			// ...
			oauth2Login { }
			oauth2Client { }
		}
	}

}

除了上述配置之外,应用程序还需要至少配置一个 ClientRegistration,方法是使用 ReactiveClientRegistrationRepository bean。以下示例使用 Spring Boot 配置属性配置 InMemoryReactiveClientRegistrationRepository bean

spring:
  security:
    oauth2:
      client:
        registration:
          my-combined-client:
            provider: my-auth-server
            client-id: my-client-id
            client-secret: my-client-secret
            authorization-grant-type: authorization_code
            scope: openid,profile,message.read,message.write
        provider:
          my-auth-server:
            issuer-uri: https://my-auth-server.com

此示例与之前的示例(使用 OAuth2 登录用户访问受保护的资源)之间的主要区别在于通过 scope 属性配置的内容,它将标准范围 openidprofile 与自定义范围 message.readmessage.write 结合起来。

除了配置 Spring Security 以支持 OAuth2 客户端功能外,您还需要确定将如何访问受保护的资源并相应地配置您的应用程序。Spring Security 提供了 ReactiveOAuth2AuthorizedClientManager 的实现,用于获取可用于访问受保护资源的访问令牌。

当不存在 ReactiveOAuth2AuthorizedClientManager 时,Spring Security 会为您注册一个默认的 ReactiveOAuth2AuthorizedClientManager bean。

使用 ReactiveOAuth2AuthorizedClientManager 的最简单方法是通过一个 ExchangeFilterFunction,该函数通过 WebClient 拦截请求。

以下示例使用默认的 ReactiveOAuth2AuthorizedClientManager 配置一个 WebClient,该 WebClient 能够通过在每个请求的 Authorization 标头中放置 Bearer 令牌来访问受保护的资源

使用 ExchangeFilterFunction 配置 WebClient
  • Java

  • Kotlin

@Configuration
public class WebClientConfig {

	@Bean
	public WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
		ServerOAuth2AuthorizedClientExchangeFilterFunction filter =
				new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
		return WebClient.builder()
				.filter(filter)
				.build();
	}

}
@Configuration
class WebClientConfig {

	@Bean
	fun webClient(authorizedClientManager: ReactiveOAuth2AuthorizedClientManager): WebClient {
		val filter = ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager)
		return WebClient.builder()
			.filter(filter)
			.build()
	}

}

此配置的 WebClient 可以像以下示例中那样使用

使用 WebClient 访问受保护的资源(当前用户)
  • Java

  • Kotlin

@RestController
public class MessagesController {

	private final WebClient webClient;

	public MessagesController(WebClient webClient) {
		this.webClient = webClient;
	}

	@GetMapping("/messages")
	public Mono<ResponseEntity<List<Message>>> messages() {
		return this.webClient.get()
				.uri("https://127.0.0.1:8090/messages")
				.retrieve()
				.toEntityList(Message.class);
	}

	public record Message(String message) {
	}

}
@RestController
class MessagesController(private val webClient: WebClient) {

	@GetMapping("/messages")
	fun messages(): Mono<ResponseEntity<List<Message>>> {
		return webClient.get()
			.uri("https://127.0.0.1:8090/messages")
			.retrieve()
			.toEntityList<Message>()
	}

	data class Message(val message: String)

}

前面的示例不同,请注意我们不需要告诉 Spring Security 我们想要使用的 clientRegistrationId。这是因为它可以从当前登录的用户那里推断出来。

启用扩展授权类型

一个常见的用例涉及启用和/或配置扩展授权类型。例如,Spring Security 支持 jwt-bearertoken-exchange 授权类型,但默认情况下不会启用它们,因为它们不是核心 OAuth 2.0 规范的一部分。

使用 Spring Security 6.3 及更高版本,我们可以简单地发布一个或多个 ReactiveOAuth2AuthorizedClientProvider 的 bean,它们将被自动获取。以下示例仅启用 jwt-bearer 授权类型

启用 jwt-bearer 授权类型
  • Java

  • Kotlin

@Configuration
public class SecurityConfig {

	@Bean
	public ReactiveOAuth2AuthorizedClientProvider jwtBearer() {
		return new JwtBearerReactiveOAuth2AuthorizedClientProvider();
	}

}
@Configuration
class SecurityConfig {

	@Bean
	fun jwtBearer(): ReactiveOAuth2AuthorizedClientProvider {
		return JwtBearerReactiveOAuth2AuthorizedClientProvider()
	}

}

当尚未提供 ReactiveOAuth2AuthorizedClientManager 时,Spring Security 将自动发布一个默认的 ReactiveOAuth2AuthorizedClientManager

任何自定义的 OAuth2AuthorizedClientProvider bean 也将被获取并应用于提供的 ReactiveOAuth2AuthorizedClientManager,位于默认授权类型之后。

为了在 Spring Security 6.3 之前实现上述配置,我们必须自己发布此 bean 并确保我们也重新启用了默认授权类型。为了理解幕后配置的内容,以下是配置可能是什么样子

启用 jwt-bearer 授权类型(6.3 之前)
  • Java

  • Kotlin

@Configuration
public class SecurityConfig {

	@Bean
	public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
			ReactiveClientRegistrationRepository clientRegistrationRepository,
			ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {

		ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
			ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
				.authorizationCode()
				.refreshToken()
				.clientCredentials()
				.password()
				.provider(new JwtBearerReactiveOAuth2AuthorizedClientProvider())
				.build();

		DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager =
			new DefaultReactiveOAuth2AuthorizedClientManager(
				clientRegistrationRepository, authorizedClientRepository);
		authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

		return authorizedClientManager;
	}

}
@Configuration
class SecurityConfig {

	@Bean
	fun authorizedClientManager(
		clientRegistrationRepository: ReactiveClientRegistrationRepository,
		authorizedClientRepository: ServerOAuth2AuthorizedClientRepository
	): ReactiveOAuth2AuthorizedClientManager {
		val authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
			.authorizationCode()
			.refreshToken()
			.clientCredentials()
			.password()
			.provider(JwtBearerReactiveOAuth2AuthorizedClientProvider())
			.build()

		val authorizedClientManager = DefaultReactiveOAuth2AuthorizedClientManager(
			clientRegistrationRepository, authorizedClientRepository
		)
		authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)

		return authorizedClientManager
	}

}

自定义现有授权类型

通过发布 bean 启用扩展授权类型的功能也提供了自定义现有授权类型的机会,而无需重新定义默认值。例如,如果我们想自定义 ReactiveOAuth2AuthorizedClientProvider 的时钟偏差 client_credentials 授权,我们可以简单地发布一个这样的 bean

自定义客户端凭据授权类型
  • Java

  • Kotlin

@Configuration
public class SecurityConfig {

	@Bean
	public ReactiveOAuth2AuthorizedClientProvider clientCredentials() {
		ClientCredentialsReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
				new ClientCredentialsReactiveOAuth2AuthorizedClientProvider();
		authorizedClientProvider.setClockSkew(Duration.ofMinutes(5));

		return authorizedClientProvider;
	}

}
@Configuration
class SecurityConfig {

	@Bean
	fun clientCredentials(): ReactiveOAuth2AuthorizedClientProvider {
		val authorizedClientProvider = ClientCredentialsReactiveOAuth2AuthorizedClientProvider()
		authorizedClientProvider.setClockSkew(Duration.ofMinutes(5))
		return authorizedClientProvider
	}

}

自定义令牌请求参数

在获取访问令牌时自定义请求参数的需求相当普遍。例如,假设我们想向令牌请求添加一个自定义的 audience 参数,因为提供程序在 authorization_code 授权中需要此参数。

我们可以简单地发布一个类型为 ReactiveOAuth2AccessTokenResponseClient 的 bean,其泛型类型为 OAuth2AuthorizationCodeGrantRequest,它将被 Spring Security 用于配置 OAuth2 客户端组件。

以下示例自定义了 authorization_code 授权的令牌请求参数

自定义授权码授权的令牌请求参数
  • Java

  • Kotlin

@Configuration
public class SecurityConfig {

	@Bean
	public ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> authorizationCodeAccessTokenResponseClient() {
		WebClientReactiveAuthorizationCodeTokenResponseClient accessTokenResponseClient =
			new WebClientReactiveAuthorizationCodeTokenResponseClient();
		accessTokenResponseClient.addParametersConverter(parametersConverter());

		return accessTokenResponseClient;
	}

	private static Converter<OAuth2AuthorizationCodeGrantRequest, MultiValueMap<String, String>> parametersConverter() {
		return (grantRequest) -> {
			MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
			parameters.set("audience", "xyz_value");

			return parameters;
		};
	}

}
@Configuration
class SecurityConfig {

	@Bean
	fun authorizationCodeAccessTokenResponseClient(): ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
		val accessTokenResponseClient = WebClientReactiveAuthorizationCodeTokenResponseClient()
		accessTokenResponseClient.addParametersConverter(parametersConverter())

		return accessTokenResponseClient
	}

	private fun parametersConverter(): Converter<OAuth2AuthorizationCodeGrantRequest, MultiValueMap<String, String>> {
		return Converter<OAuth2AuthorizationCodeGrantRequest, MultiValueMap<String, String>> { grantRequest ->
			LinkedMultiValueMap<String, String>().also { parameters ->
				parameters["audience"] = "xyz_value"
			}
		}
	}

}

请注意,在这种情况下,我们不需要自定义 SecurityWebFilterChain bean,并且可以使用默认值。如果使用 Spring Boot 且没有其他自定义,我们实际上可以完全省略 SecurityWebFilterChain bean。

如您所见,提供 ReactiveOAuth2AccessTokenResponseClient 作为 bean 非常方便。当直接使用 Spring Security DSL 时,我们需要确保此自定义适用于 OAuth2 登录(如果我们正在使用此功能)和 OAuth2 客户端组件。为了理解幕后配置的内容,以下是使用 DSL 的配置

使用 DSL 自定义授权码授权的令牌请求参数
  • Java

  • Kotlin

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

	@Bean
	public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
		WebClientReactiveAuthorizationCodeTokenResponseClient accessTokenResponseClient =
			new WebClientReactiveAuthorizationCodeTokenResponseClient();
		accessTokenResponseClient.addParametersConverter(parametersConverter());

		http
			.authorizeExchange((authorize) -> authorize
				.anyExchange().authenticated()
			)
			.oauth2Login((oauth2Login) -> oauth2Login
				.authenticationManager(new DelegatingReactiveAuthenticationManager(
					new OidcAuthorizationCodeReactiveAuthenticationManager(
						accessTokenResponseClient, new OidcReactiveOAuth2UserService()
					),
					new OAuth2LoginReactiveAuthenticationManager(
						accessTokenResponseClient, new DefaultReactiveOAuth2UserService()
					)
				))
			)
			.oauth2Client((oauth2Client) -> oauth2Client
				.authenticationManager(new OAuth2AuthorizationCodeReactiveAuthenticationManager(
					accessTokenResponseClient
				))
			);

		return http.build();
	}

	private static Converter<OAuth2AuthorizationCodeGrantRequest, MultiValueMap<String, String>> parametersConverter() {
		// ...
	}

}
import org.springframework.security.config.web.server.invoke

@Configuration
@EnableWebFluxSecurity
class SecurityConfig {

	@Bean
	fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
		val accessTokenResponseClient = WebClientReactiveAuthorizationCodeTokenResponseClient()
		accessTokenResponseClient.addParametersConverter(parametersConverter())

		return http {
			authorizeExchange {
				authorize(anyExchange, authenticated)
			}
			oauth2Login {
				authenticationManager = DelegatingReactiveAuthenticationManager(
					OidcAuthorizationCodeReactiveAuthenticationManager(
						accessTokenResponseClient, OidcReactiveOAuth2UserService()
					),
					OAuth2LoginReactiveAuthenticationManager(
						accessTokenResponseClient, DefaultReactiveOAuth2UserService()
					)
				)
			}
			oauth2Client {
				authenticationManager = OAuth2AuthorizationCodeReactiveAuthenticationManager(
					accessTokenResponseClient
				)
			}
		}
	}

	private fun parametersConverter(): Converter<OAuth2AuthorizationCodeGrantRequest, MultiValueMap<String, String>> {
		// ...
	}

}

对于其他授权类型,我们可以发布额外的 ReactiveOAuth2AccessTokenResponseClient bean 以覆盖默认值。例如,要自定义 client_credentials 授权的令牌请求,我们可以发布以下 bean

自定义客户端凭据授权的令牌请求参数
  • Java

  • Kotlin

@Configuration
public class SecurityConfig {

	@Bean
	public ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsAccessTokenResponseClient() {
		WebClientReactiveClientCredentialsTokenResponseClient accessTokenResponseClient =
				new WebClientReactiveClientCredentialsTokenResponseClient();
		accessTokenResponseClient.addParametersConverter(parametersConverter());

		return accessTokenResponseClient;
	}

	private static Converter<OAuth2ClientCredentialsGrantRequest, MultiValueMap<String, String>> parametersConverter() {
		// ...
	}

}
@Configuration
class SecurityConfig {

	@Bean
	fun clientCredentialsAccessTokenResponseClient(): ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> {
		val accessTokenResponseClient = WebClientReactiveClientCredentialsTokenResponseClient()
		accessTokenResponseClient.addParametersConverter(parametersConverter())

		return accessTokenResponseClient
	}

	private fun parametersConverter(): Converter<OAuth2ClientCredentialsGrantRequest, MultiValueMap<String, String>> {
		// ...
	}

}

Spring Security 自动解析以下 ReactiveOAuth2AccessTokenResponseClient bean 的泛型类型

  • OAuth2AuthorizationCodeGrantRequest(请参阅 WebClientReactiveAuthorizationCodeTokenResponseClient

  • OAuth2RefreshTokenGrantRequest(参见WebClientReactiveRefreshTokenTokenResponseClient

  • OAuth2ClientCredentialsGrantRequest(参见WebClientReactiveClientCredentialsTokenResponseClient

  • OAuth2PasswordGrantRequest(参见WebClientReactivePasswordTokenResponseClient

  • JwtBearerGrantRequest(参见WebClientReactiveJwtBearerTokenResponseClient

  • TokenExchangeGrantRequest(参见WebClientReactiveTokenExchangeTokenResponseClient

发布类型为ReactiveOAuth2AccessTokenResponseClient<JwtBearerGrantRequest>的 Bean 将自动启用jwt-bearer授权类型,无需单独配置

发布类型为ReactiveOAuth2AccessTokenResponseClient<TokenExchangeGrantRequest>的 Bean 将自动启用token-exchange授权类型,无需单独配置

自定义 OAuth2 客户端组件使用的WebClient

另一个常见用例是需要自定义获取访问令牌时使用的WebClient。我们可能需要这样做来自定义底层 HTTP 客户端库(通过自定义的ClientHttpConnector)以配置 SSL 设置或为公司网络应用代理设置。

在 Spring Security 6.3 及更高版本中,我们可以简单地发布类型为ReactiveOAuth2AccessTokenResponseClient的 Bean,Spring Security 将为我们配置和发布一个ReactiveOAuth2AuthorizedClientManager Bean。

以下示例自定义了所有支持的授权类型的WebClient

自定义 OAuth2 客户端的WebClient
  • Java

  • Kotlin

@Configuration
public class SecurityConfig {

	@Bean
	public ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> authorizationCodeAccessTokenResponseClient() {
		WebClientReactiveAuthorizationCodeTokenResponseClient accessTokenResponseClient =
			new WebClientReactiveAuthorizationCodeTokenResponseClient();
		accessTokenResponseClient.setWebClient(webClient());

		return accessTokenResponseClient;
	}

	@Bean
	public ReactiveOAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenAccessTokenResponseClient() {
		WebClientReactiveRefreshTokenTokenResponseClient accessTokenResponseClient =
			new WebClientReactiveRefreshTokenTokenResponseClient();
		accessTokenResponseClient.setWebClient(webClient());

		return accessTokenResponseClient;
	}

	@Bean
	public ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsAccessTokenResponseClient() {
		WebClientReactiveClientCredentialsTokenResponseClient accessTokenResponseClient =
			new WebClientReactiveClientCredentialsTokenResponseClient();
		accessTokenResponseClient.setWebClient(webClient());

		return accessTokenResponseClient;
	}

	@Bean
	public ReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> passwordAccessTokenResponseClient() {
		WebClientReactivePasswordTokenResponseClient accessTokenResponseClient =
			new WebClientReactivePasswordTokenResponseClient();
		accessTokenResponseClient.setWebClient(webClient());

		return accessTokenResponseClient;
	}

	@Bean
	public ReactiveOAuth2AccessTokenResponseClient<JwtBearerGrantRequest> jwtBearerAccessTokenResponseClient() {
		WebClientReactiveJwtBearerTokenResponseClient accessTokenResponseClient =
			new WebClientReactiveJwtBearerTokenResponseClient();
		accessTokenResponseClient.setWebClient(webClient());

		return accessTokenResponseClient;
	}

	@Bean
	public ReactiveOAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> tokenExchangeAccessTokenResponseClient() {
		WebClientReactiveTokenExchangeTokenResponseClient accessTokenResponseClient =
			new WebClientReactiveTokenExchangeTokenResponseClient();
		accessTokenResponseClient.setWebClient(webClient());

		return accessTokenResponseClient;
	}

	@Bean
	public WebClient webClient() {
		// ...
	}

}
@Configuration
class SecurityConfig {

	@Bean
	fun authorizationCodeAccessTokenResponseClient(): ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
		val accessTokenResponseClient = WebClientReactiveAuthorizationCodeTokenResponseClient()
		accessTokenResponseClient.setWebClient(webClient())

		return accessTokenResponseClient
	}

	@Bean
	fun refreshTokenAccessTokenResponseClient(): ReactiveOAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> {
		val accessTokenResponseClient = WebClientReactiveRefreshTokenTokenResponseClient()
		accessTokenResponseClient.setWebClient(webClient())

		return accessTokenResponseClient
	}

	@Bean
	fun clientCredentialsAccessTokenResponseClient(): ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> {
		val accessTokenResponseClient = WebClientReactiveClientCredentialsTokenResponseClient()
		accessTokenResponseClient.setWebClient(webClient())

		return accessTokenResponseClient
	}

	@Bean
	fun passwordAccessTokenResponseClient(): ReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> {
		val accessTokenResponseClient = WebClientReactivePasswordTokenResponseClient()
		accessTokenResponseClient.setWebClient(webClient())

		return accessTokenResponseClient
	}

	@Bean
	fun jwtBearerAccessTokenResponseClient(): ReactiveOAuth2AccessTokenResponseClient<JwtBearerGrantRequest> {
		val accessTokenResponseClient = WebClientReactiveJwtBearerTokenResponseClient()
		accessTokenResponseClient.setWebClient(webClient())

		return accessTokenResponseClient
	}

	@Bean
	fun tokenExchangeAccessTokenResponseClient(): ReactiveOAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> {
		val accessTokenResponseClient = WebClientReactiveTokenExchangeTokenResponseClient()
		accessTokenResponseClient.setWebClient(webClient())

		return accessTokenResponseClient
	}

	@Bean
	fun webClient(): WebClient {
		// ...
	}

}

当尚未提供 ReactiveOAuth2AuthorizedClientManager 时,Spring Security 将自动发布一个默认的 ReactiveOAuth2AuthorizedClientManager

请注意,在这种情况下,我们不需要自定义 SecurityWebFilterChain bean,并且可以使用默认值。如果使用 Spring Boot 且没有其他自定义,我们实际上可以完全省略 SecurityWebFilterChain bean。

在 Spring Security 6.3 之前,我们必须自己确保此自定义应用于 OAuth2 客户端组件。虽然我们可以为authorization_code授权类型发布类型为ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest>的 Bean,但我们必须为其他授权类型发布类型为ReactiveOAuth2AuthorizedClientManager的 Bean。为了了解幕后配置的内容,以下是配置可能的样子

自定义 OAuth2 客户端的WebClient(6.3 之前)
  • Java

  • Kotlin

@Configuration
public class SecurityConfig {

	@Bean
	public ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> authorizationCodeAccessTokenResponseClient() {
		WebClientReactiveAuthorizationCodeTokenResponseClient accessTokenResponseClient =
			new WebClientReactiveAuthorizationCodeTokenResponseClient();
		accessTokenResponseClient.setWebClient(webClient());

		return accessTokenResponseClient;
	}

	@Bean
	public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
			ReactiveClientRegistrationRepository clientRegistrationRepository,
			ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {

		WebClientReactiveRefreshTokenTokenResponseClient refreshTokenAccessTokenResponseClient =
			new WebClientReactiveRefreshTokenTokenResponseClient();
		refreshTokenAccessTokenResponseClient.setWebClient(webClient());

		WebClientReactiveClientCredentialsTokenResponseClient clientCredentialsAccessTokenResponseClient =
			new WebClientReactiveClientCredentialsTokenResponseClient();
		clientCredentialsAccessTokenResponseClient.setWebClient(webClient());

		WebClientReactivePasswordTokenResponseClient passwordAccessTokenResponseClient =
			new WebClientReactivePasswordTokenResponseClient();
		passwordAccessTokenResponseClient.setWebClient(webClient());

		WebClientReactiveJwtBearerTokenResponseClient jwtBearerAccessTokenResponseClient =
			new WebClientReactiveJwtBearerTokenResponseClient();
		jwtBearerAccessTokenResponseClient.setWebClient(webClient());

		JwtBearerReactiveOAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider =
			new JwtBearerReactiveOAuth2AuthorizedClientProvider();
		jwtBearerAuthorizedClientProvider.setAccessTokenResponseClient(jwtBearerAccessTokenResponseClient);

		WebClientReactiveTokenExchangeTokenResponseClient tokenExchangeAccessTokenResponseClient =
			new WebClientReactiveTokenExchangeTokenResponseClient();
		tokenExchangeAccessTokenResponseClient.setWebClient(webClient());

		TokenExchangeReactiveOAuth2AuthorizedClientProvider tokenExchangeAuthorizedClientProvider =
			new TokenExchangeReactiveOAuth2AuthorizedClientProvider();
		tokenExchangeAuthorizedClientProvider.setAccessTokenResponseClient(tokenExchangeAccessTokenResponseClient);

		ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
			ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
				.authorizationCode()
				.refreshToken((refreshToken) -> refreshToken
					.accessTokenResponseClient(refreshTokenAccessTokenResponseClient)
				)
				.clientCredentials((clientCredentials) -> clientCredentials
					.accessTokenResponseClient(clientCredentialsAccessTokenResponseClient)
				)
				.password((password) -> password
					.accessTokenResponseClient(passwordAccessTokenResponseClient)
				)
				.provider(jwtBearerAuthorizedClientProvider)
				.provider(tokenExchangeAuthorizedClientProvider)
				.build();

		DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager =
			new DefaultReactiveOAuth2AuthorizedClientManager(
				clientRegistrationRepository, authorizedClientRepository);
		authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

		return authorizedClientManager;
	}

	@Bean
	public WebClient webClient() {
		// ...
	}

}
import org.springframework.security.config.web.server.invoke

@Configuration
class SecurityConfig {

	@Bean
	fun authorizationCodeAccessTokenResponseClient(): ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
		val accessTokenResponseClient = WebClientReactiveAuthorizationCodeTokenResponseClient()
		accessTokenResponseClient.setWebClient(webClient())

		return accessTokenResponseClient
	}

	@Bean
	fun authorizedClientManager(
		clientRegistrationRepository: ReactiveClientRegistrationRepository?,
		authorizedClientRepository: ServerOAuth2AuthorizedClientRepository?
	): ReactiveOAuth2AuthorizedClientManager {
		val refreshTokenAccessTokenResponseClient = WebClientReactiveRefreshTokenTokenResponseClient()
		refreshTokenAccessTokenResponseClient.setWebClient(webClient())

		val clientCredentialsAccessTokenResponseClient = WebClientReactiveClientCredentialsTokenResponseClient()
		clientCredentialsAccessTokenResponseClient.setWebClient(webClient())

		val passwordAccessTokenResponseClient = WebClientReactivePasswordTokenResponseClient()
		passwordAccessTokenResponseClient.setWebClient(webClient())

		val jwtBearerAccessTokenResponseClient = WebClientReactiveJwtBearerTokenResponseClient()
		jwtBearerAccessTokenResponseClient.setWebClient(webClient())

		val jwtBearerAuthorizedClientProvider = JwtBearerReactiveOAuth2AuthorizedClientProvider()
		jwtBearerAuthorizedClientProvider.setAccessTokenResponseClient(jwtBearerAccessTokenResponseClient)

		val tokenExchangeAccessTokenResponseClient = WebClientReactiveTokenExchangeTokenResponseClient()
		tokenExchangeAccessTokenResponseClient.setWebClient(webClient())

		val tokenExchangeAuthorizedClientProvider = TokenExchangeReactiveOAuth2AuthorizedClientProvider()
		tokenExchangeAuthorizedClientProvider.setAccessTokenResponseClient(tokenExchangeAccessTokenResponseClient)

		val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
			.authorizationCode()
			.refreshToken { refreshToken ->
				refreshToken.accessTokenResponseClient(refreshTokenAccessTokenResponseClient)
			}
			.clientCredentials { clientCredentials ->
				clientCredentials.accessTokenResponseClient(clientCredentialsAccessTokenResponseClient)
			}
			.password { password ->
				password.accessTokenResponseClient(passwordAccessTokenResponseClient)
			}
			.provider(jwtBearerAuthorizedClientProvider)
			.provider(tokenExchangeAuthorizedClientProvider)
			.build()

		val authorizedClientManager = DefaultReactiveOAuth2AuthorizedClientManager(
			clientRegistrationRepository, authorizedClientRepository
		)
		authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)

		return authorizedClientManager
	}

	@Bean
	fun webClient(): WebClient {
		// ...
	}

}

进一步阅读

前面的部分介绍了 Spring Security 对 OAuth2 的支持,并提供了常见场景的示例。您可以在参考文档的以下部分阅读有关 OAuth2 客户端和资源服务器的更多信息