OAuth2 WebFlux

Spring Security 提供全面的 OAuth 2.0 支援。本節討論如何將 OAuth 2.0 整合到您的反應式應用程式中。

概觀

Spring Security 的 OAuth 2.0 支援包含兩個主要功能集

OAuth2 登入是一個非常強大的 OAuth2 Client 功能,值得在參考文件中單獨介紹。然而,它並非獨立存在的功能,需要 OAuth2 Client 才能運作。

這些功能集涵蓋了 OAuth 2.0 授權框架中定義的資源伺服器和 client 角色,而授權伺服器角色則由 Spring Authorization Server 涵蓋,Spring Authorization Server 是一個基於 Spring Security 建構的獨立專案。

OAuth2 中的資源伺服器和 client 角色通常由一個或多個伺服器端應用程式表示。此外,授權伺服器角色可以由一個或多個第三方表示(例如在組織內集中管理身份和/或驗證的情況),或者由一個應用程式表示(例如 Spring Authorization Server 的情況)。

例如,典型的基於 OAuth2 的微服務架構可能包含一個面向使用者的 client 應用程式、多個提供 REST API 的後端資源伺服器,以及一個用於管理使用者和身份驗證問題的第三方授權伺服器。常見的情況是,單一應用程式僅代表其中一個角色,並且需要與一個或多個提供其他角色的第三方整合。

Spring Security 可以處理這些以及更多情境。以下章節涵蓋 Spring Security 提供的角色,並包含常見情境的範例。

OAuth2 資源伺服器

本節包含 OAuth2 資源伺服器功能的摘要以及範例。如需完整的參考文件,請參閱 OAuth 2.0 資源伺服器

若要開始使用,請將 spring-security-oauth2-resource-server 相依性新增至您的專案。使用 Spring Boot 時,請新增以下 starter

搭配 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 資源伺服器的以下使用案例

使用 OAuth2 Access Token 保護存取權限

使用 OAuth2 access token 保護 API 的存取權限非常常見。在大多數情況下,Spring Security 只需要最少的設定即可使用 OAuth2 保護應用程式。

Spring Security 支援兩種 Bearer token 類型,每種類型都使用不同的元件進行驗證

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")
	}

}

不透明 Token 支援

以下範例使用 Spring Boot 設定屬性設定 OpaqueTokenIntrospector bean

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

使用 Spring Boot 時,這就是所有需要的設定。Spring Boot 提供的預設配置相當於以下設定

使用不透明 Token 設定資源伺服器
  • 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 token,包括自訂 JWT。

使用 JWT 保護 API 只需要一個 ReactiveJwtDecoder bean,它用於驗證簽章和解碼 token。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 設定資源伺服器
  • 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 沒有提供用於產生 token 的端點。但是,Spring Security 提供了 JwtEncoder 介面以及一個實作,即 NimbusJwtEncoder

OAuth2 Client

本節包含 OAuth2 Client 功能的摘要以及範例。如需完整的參考文件,請參閱 OAuth 2.0 ClientOAuth 2.0 登入

若要開始使用,請將 spring-security-oauth2-client 相依性新增至您的專案。使用 Spring Boot 時,請新增以下 starter

搭配 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 的特殊 token,其設計目的是為 OAuth2 Client 提供執行使用者身份驗證和讓使用者登入的能力。在某些情況下,OAuth2 可以直接用於讓使用者登入 (例如,對於不實作 OpenID Connect 的熱門社群登入提供者,如 GitHub 和 Facebook)。

以下範例設定應用程式作為 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 { }
		}
	}

}

除了上述設定之外,應用程式還需要至少一個 ClientRegistration,透過使用 ReactiveClientRegistrationRepository bean 進行設定。以下範例使用 Spring Boot 設定屬性設定 InMemoryReactiveClientRegistrationRepository bean

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

透過上述設定,應用程式現在支援兩個額外的端點

  1. 登入端點 (例如 /oauth2/authorization/my-oidc-client) 用於啟動登入並執行重新導向至第三方授權伺服器。

  2. 重新導向端點 (例如 /login/oauth2/code/my-oidc-client) 由授權伺服器用於重新導向回 client 應用程式,並且將包含一個 code 參數,用於透過 access token 請求取得 id_token 和/或 access_token

上述設定中存在 openid scope 表示應使用 OpenID Connect 1.0。這會指示 Spring Security 在請求處理期間使用 OIDC 專用元件 (例如 OidcReactiveOAuth2UserService)。如果沒有此 scope,Spring Security 將改為使用 OAuth2 專用元件 (例如 DefaultReactiveOAuth2UserService)。

存取受保護的資源

向受 OAuth2 保護的第三方 API 發出請求是 OAuth2 Client 的核心使用案例。這是透過授權 client (由 Spring Security 中的 OAuth2AuthorizedClient 類別表示),並透過在輸出請求的 Authorization 標頭中放置 Bearer token 來存取受保護的資源來完成的。

以下範例設定應用程式作為 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() 的範例。

除了上述設定之外,應用程式還需要至少一個 ClientRegistration,透過使用 ReactiveClientRegistrationRepository bean 進行設定。以下範例使用 Spring Boot 設定屬性設定 InMemoryReactiveClientRegistrationRepository bean

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

除了設定 Spring Security 以支援 OAuth2 Client 功能外,您還需要決定如何存取受保護的資源,並據此設定您的應用程式。Spring Security 提供了 ReactiveOAuth2AuthorizedClientManager 的實作,用於取得可用於存取受保護資源的 access token。

當不存在 ReactiveOAuth2AuthorizedClientManager bean 時,Spring Security 會為您註冊一個預設的 bean。

使用 ReactiveOAuth2AuthorizedClientManager 最簡單的方式是透過 ExchangeFilterFunction,它會攔截透過 WebClient 的請求。

以下範例使用預設的 ReactiveOAuth2AuthorizedClientManager 設定 WebClient,使其能夠透過在每個請求的 Authorization 標頭中放置 Bearer token 來存取受保護的資源

使用 ExchangeFilterFunction 設定 WebClient
  • Java

  • Kotlin

@Configuration
public class WebClientConfig {

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

}
@Configuration
class WebClientConfig {

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

}

這個設定好的 WebClient 可以像以下範例中那樣使用

使用 WebClient 存取受保護的資源
  • Java

  • Kotlin

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

@RestController
public class MessagesController {

	private final WebClient webClient;

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

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

	public record Message(String message) {
	}

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

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

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

	data class Message(val message: String)

}

為目前使用者存取受保護的資源

當使用者透過 OAuth2 或 OpenID Connect 登入時,授權伺服器可能會提供一個 access token,可以直接用於存取受保護的資源。這很方便,因為它只需要設定單一 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 { }
		}
	}

}

除了上述設定之外,應用程式還需要至少一個 ClientRegistration,透過使用 ReactiveClientRegistrationRepository bean 進行設定。以下範例使用 Spring Boot 設定屬性設定 InMemoryReactiveClientRegistrationRepository bean

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

前面的範例 (「使用 OAuth2 讓使用者登入」、「存取受保護的資源」) 與此範例之間的主要區別在於透過 scope 屬性設定的內容,它結合了標準 scope openidprofile 以及自訂 scope message.readmessage.write

除了設定 Spring Security 以支援 OAuth2 Client 功能外,您還需要決定如何存取受保護的資源,並據此設定您的應用程式。Spring Security 提供了 ReactiveOAuth2AuthorizedClientManager 的實作,用於取得可用於存取受保護資源的 access token。

當不存在 ReactiveOAuth2AuthorizedClientManager bean 時,Spring Security 會為您註冊一個預設的 bean。

使用 ReactiveOAuth2AuthorizedClientManager 最簡單的方式是透過 ExchangeFilterFunction,它會攔截透過 WebClient 的請求。

以下範例使用預設的 ReactiveOAuth2AuthorizedClientManager 設定 WebClient,使其能夠透過在每個請求的 Authorization 標頭中放置 Bearer token 來存取受保護的資源

使用 ExchangeFilterFunction 設定 WebClient
  • Java

  • Kotlin

@Configuration
public class WebClientConfig {

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

}
@Configuration
class WebClientConfig {

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

}

這個設定好的 WebClient 可以像以下範例中那樣使用

使用 WebClient 存取受保護的資源 (目前使用者)
  • Java

  • Kotlin

@RestController
public class MessagesController {

	private final WebClient webClient;

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

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

	public record Message(String message) {
	}

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

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

	data class Message(val message: String)

}

先前的範例不同,請注意我們不需要告知 Spring Security 我們想要使用的 clientRegistrationId。這是因為它可以從目前登入的使用者推導出來。

啟用擴充授權類型

常見的使用案例包括啟用和/或設定擴充授權類型。例如,Spring Security 支援 jwt-bearertoken-exchange 授權類型,但預設情況下不啟用它們,因為它們不是核心 OAuth 2.0 規範的一部分。

在 Spring Security 6.3 及更新版本中,我們只需發布一個或多個 ReactiveOAuth2AuthorizedClientProvider 的 bean,它們就會自動被選取。以下範例僅啟用 jwt-bearer 授權類型

啟用 jwt-bearer 授權類型
  • Java

  • Kotlin

@Configuration
public class SecurityConfig {

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

}
@Configuration
class SecurityConfig {

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

}

當尚未提供 ReactiveOAuth2AuthorizedClientManager 時,Spring Security 將自動發布預設的 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()
				.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 來啟用擴充授權類型的能力也提供了自訂現有授權類型的機會,而無需重新定義預設值。例如,如果我們想要自訂 ReactiveOAuth2AuthorizedClientProviderclient_credentials 授權的時鐘偏差,我們可以簡單地發布一個 bean,如下所示

自訂 Client Credentials 授權類型
  • 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
	}

}

自訂 Token 請求參數

在取得 access token 時,自訂請求參數的需求相當常見。例如,假設我們想要將自訂 audience 參數新增至 token 請求,因為提供者需要此參數用於 authorization_code 授權。

我們可以簡單地發布類型為 ReactiveOAuth2AccessTokenResponseClient 且泛型類型為 OAuth2AuthorizationCodeGrantRequest 的 bean,Spring Security 將使用它來設定 OAuth2 Client 元件。

以下範例自訂 authorization_code 授權的 token 請求參數

自訂 Authorization Code 授權的 Token 請求參數
  • 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 自訂 Authorization Code 授權的 Token 請求參數
  • 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 授權的 token 請求,我們可以發布以下 bean

自訂 Client Credentials 授權的 Token 請求參數
  • 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

另一個常見的使用案例是需要自訂在取得 access token 時使用的 WebClient。我們可能需要這樣做,以自訂底層 HTTP client 程式庫 (透過自訂 ClientHttpConnector) 來設定 SSL 設定或為企業網路套用 proxy 設定。

在 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 時,Spring Security 將自動發布預設的 bean。

請注意,在這種情況下,我們不需要自訂 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 和資源伺服器的資訊