OAuth2 WebFlux

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

概览

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

OAuth2 Login 是一个非常强大的 OAuth2 Client 功能,值得在参考文档中专门介绍。然而,它并非独立功能,需要 OAuth2 Client 才能正常工作。

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

OAuth2 中的 *资源服务器* 和 *客户端* 角色通常由一个或多个服务器端应用表示。此外,*授权服务器* 角色可以由一个或多个第三方表示(例如在组织内部集中身份管理和/或认证),**-或者-** 也可以由一个应用表示(例如 Spring Authorization Server)。

例如,典型的基于 OAuth2 的微服务架构可能包含一个面向用户的客户端应用、几个提供 REST API 的后端资源服务器以及一个用于管理用户和认证的第三方授权服务器。常见的情况是,单个应用仅代表其中一个角色,需要与提供其他角色的一或多个第三方集成。

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

OAuth2 Resource Server

本节包含 OAuth2 Resource Server 功能概述及示例。更多完整的参考文档请参阅 OAuth 2.0 Resource Server

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

配合 Spring Boot 的 OAuth2 Client
  • 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 Resource Server 的使用场景

使用 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 配置 Resource Server
  • 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 配置属性配置一个 ReactiveOpaqueTokenIntrospector 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 提供的默认配置等同于以下内容

使用不透明令牌配置 Resource Server
  • 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 Resource Server 支持可用于任何类型的 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

你可以将公钥作为 classpath 资源提供(本例中名为 my-public-key.pub)。

使用 Spring Boot 时,只需这些配置。Spring Boot 提供的默认配置等同于以下内容

使用自定义 JWT 配置 Resource Server
  • 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 Client

本节包含 OAuth2 Client 功能概述及示例。更多完整的参考文档请参阅 OAuth 2.0 ClientOAuth 2.0 登录

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

配合 Spring Boot 的 OAuth2 Client
  • 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 Client 的使用场景

使用 OAuth2 让用户登录

要求用户通过 OAuth2 登录非常常见。OpenID Connect 1.0 提供了一个名为 id_token 的特殊令牌,旨在赋予 OAuth2 Client 执行用户身份验证和让用户登录的能力。在某些情况下,OAuth2 可以直接用于让用户登录(例如,GitHub 和 Facebook 等未实现 OpenID Connect 的流行社交登录提供商)。

以下示例将应用配置为 OAuth2 Client,能够使用 OAuth2 或 OpenID Connect 让用户登录

配置 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 { }
		}
	}

}

除了上述配置,应用还需要通过使用 ReactiveClientRegistrationRepository bean 来配置至少一个 ClientRegistration。以下示例使用 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 scope 的存在表明应使用 OpenID Connect 1.0。这指示 Spring Security 在请求处理期间使用 OIDC 特定组件(例如 OidcReactiveOAuth2UserService)。如果没有此 scope,Spring Security 将转而使用 OAuth2 特定组件(例如 DefaultReactiveOAuth2UserService)。

访问受保护资源

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

以下示例将应用配置为 OAuth2 Client,能够从第三方 API 请求受保护资源

配置 OAuth2 Client
  • 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() 结合使用的示例

除了上述配置,应用还需要通过使用 ReactiveClientRegistrationRepository bean 来配置至少一个 ClientRegistration。以下示例使用 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 Client 功能外,你还需要决定如何访问受保护资源并相应地配置你的应用。Spring Security 提供了 ReactiveOAuth2AuthorizedClientManager 的实现,用于获取可用于访问受保护资源的访问令牌。

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

使用 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("http://localhost: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("http://localhost:8090/messages")
			.attributes(clientRegistrationId("my-oauth2-client"))
			.retrieve()
			.toEntityList<Message>()
	}

	data class Message(val message: String)

}

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

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

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

以下示例将应用配置为 OAuth2 Client,能够让用户登录 *并* 从第三方 API 请求受保护资源

配置 OAuth2 登录和 OAuth2 Client
  • 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 { }
		}
	}

}

除了上述配置,应用还需要通过使用 ReactiveClientRegistrationRepository bean 来配置至少一个 ClientRegistration。以下示例使用 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 属性配置的内容,本例将标准 scope openidprofile 与自定义 scope message.readmessage.write 结合使用。

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

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

使用 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("http://localhost: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("http://localhost: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 bean 时,Spring Security 会自动发布一个默认的。

任何自定义的 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 启用扩展授权模式的能力也为定制现有授权模式提供了机会,而无需重新定义默认值。例如,如果我们想定制 client_credentials 授权模式的 ReactiveOAuth2AuthorizedClientProvider 的时钟偏移,我们只需像这样发布一个 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 授权模式要求此参数。

我们只需发布一个泛型类型为 OAuth2AuthorizationCodeGrantRequestReactiveOAuth2AccessTokenResponseClient 类型 bean,Spring Security 将使用它来配置 OAuth2 Client 组件。

以下示例定制了 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 Client 组件。为了理解其背后配置的原理,以下是使用 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 Client 组件使用的 WebClient

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

在 Spring Security 6.3 及更高版本中,我们只需发布 ReactiveOAuth2AccessTokenResponseClient 类型的 bean,Spring Security 将为我们配置并发布一个 ReactiveOAuth2AuthorizedClientManager bean。

以下示例为所有支持的授权模式定制了 WebClient

为 OAuth2 Client 定制 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 bean 时,Spring Security 会自动发布一个默认的。

请注意,在这种情况下我们无需定制 SecurityWebFilterChain bean,可以使用默认值。如果使用 Spring Boot 且没有额外的定制,我们甚至可以完全省略 SecurityWebFilterChain bean。

在 Spring Security 6.3 之前,我们必须自己确保将此定制应用于 OAuth2 Client 组件。虽然我们可以为 authorization_code 授权模式发布一个 ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> 类型的 bean,但对于其他授权模式,我们必须发布一个 ReactiveOAuth2AuthorizedClientManager 类型的 bean。为了理解其背后配置的原理,以下是配置示例

为 OAuth2 Client 定制 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 Client 和 Resource Server 的内容