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("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 配置登入,為另一個配置訪問受保護資源。所有此類場景都將使用相同的基本配置。

以下示例將應用配置為 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("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 會自動釋出一個預設的。

任何自定義的 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 的內容