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 支援

以下示例使用 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 配置屬性配置 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 提供的預設配置等同於以下配置

使用不透明令牌配置資源伺服器
  • 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 { }
		}
	}

}

除了上述配置,應用程式還需要透過使用 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)由授權伺服器用於重定向回客戶端應用程式,並將包含用於透過訪問令牌請求獲取 id_token 和/或 access_tokencode 引數。

上述配置中 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() 結合使用的示例,請參閱下一節

除了上述配置,應用程式還需要透過使用 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 客戶端功能之外,您還需要決定如何訪問受保護資源並相應地配置您的應用程式。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("https://: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://:8090/messages")
			.attributes(clientRegistrationId("my-oauth2-client"))
			.retrieve()
			.toEntityList<Message>()
	}

	data class Message(val message: String)

}

訪問當前使用者的受保護資源

當用戶透過 OAuth2 或 OpenID Connect 登入時,授權伺服器可能會提供一個訪問令牌,該令牌可以直接用於訪問受保護資源。這很方便,因為它只需要同時配置一個 ClientRegistration 即可滿足這兩個用例。

本節將使用 OAuth2 登入使用者訪問受保護資源合併到一個配置中。還存在其他高階場景,例如為登入配置一個 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 { }
		}
	}

}

除了上述配置,應用程式還需要透過使用 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 屬性配置的內容,它將標準範圍 openidprofile 與自定義範圍 message.readmessage.write 結合起來。

除了配置 Spring Security 以支援 OAuth2 客戶端功能之外,您還需要決定如何訪問受保護資源並相應地配置您的應用程式。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("https://: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://: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 將自動釋出一個預設的 ReactiveOAuth2AuthorizedClientManager bean。

任何自定義的 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()
				.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()
			.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 客戶端元件。

以下示例為 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

  • JwtBearerGrantRequest(參見 WebClientReactiveJwtBearerTokenResponseClient

  • TokenExchangeGrantRequest(參見 WebClientReactiveTokenExchangeTokenResponseClient

釋出 ReactiveOAuth2AccessTokenResponseClient<JwtBearerGrantRequest> 型別的 bean 將自動啟用 jwt-bearer 授權型別,而無需單獨配置它

釋出 ReactiveOAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> 型別的 bean 將自動啟用 token-exchange 授權型別,而無需單獨配置它

自定義 OAuth2 客戶端元件使用的 WebClient

另一個常見的用例是需要自定義在獲取訪問令牌時使用的 WebClient。我們可能需要這樣做來透過自定義 ClientHttpConnector 自定義底層 HTTP 客戶端庫,以配置 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<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 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 將自動釋出一個預設的 ReactiveOAuth2AuthorizedClientManager bean。

請注意,在這種情況下我們不需要自定義 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());

		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)
				)
				.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 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)
			}
			.provider(jwtBearerAuthorizedClientProvider)
			.provider(tokenExchangeAuthorizedClientProvider)
			.build()

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

		return authorizedClientManager
	}

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

}

延伸閱讀

前面的章節介紹了 Spring Security 對 OAuth2 的支援,並提供了常見場景的示例。您可以在參考文件的以下章節中閱讀更多關於 OAuth2 客戶端和資源伺服器的內容

© . This site is unofficial and not affiliated with VMware.