OAuth2

Spring Security 提供了對 OAuth 2.0 的全面支援。本節討論如何將 OAuth 2.0 整合到基於 Servlet 的應用中。

概覽

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

OAuth2 登入 是一個非常強大的 OAuth2 Client 特性,在參考文件中有專門的章節介紹。然而,它並非獨立特性,需要 OAuth2 Client 才能工作。

這些功能集涵蓋了 OAuth 2.0 授權框架 中定義的 資源伺服器客戶端 角色,而 授權伺服器 角色由 Spring Authorization Server 負責,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 時,新增以下 starter:

OAuth2 Client 與 Spring Boot
  • 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 Access Token 保護訪問

使用 OAuth2 訪問令牌保護 API 訪問非常常見。在大多數情況下,Spring Security 只需要最少的配置即可使用 OAuth2 保護應用。

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

JWT 支援

以下示例使用 Spring Boot 配置屬性配置 JwtDecoder bean

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

使用 Spring Boot 時,只需要這些配置。Spring Boot 提供的預設配置等同於以下內容

使用 JWT 配置 Resource Server
  • Java

  • Kotlin

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			.authorizeHttpRequests((authorize) -> authorize
				.anyRequest().authenticated()
			)
			.oauth2ResourceServer((oauth2) -> oauth2
				.jwt(Customizer.withDefaults())
			);
		return http.build();
	}

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

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

@Configuration
@EnableWebSecurity
class SecurityConfig {

	@Bean
	fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
		http {
			authorizeHttpRequests {
				authorize(anyRequest, authenticated)
			}
			oauth2ResourceServer {
				jwt { }
			}
		}

		return http.build()
	}

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

}

不透明令牌支援

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

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

使用 Spring Boot 時,只需要這些配置。Spring Boot 提供的預設配置等同於以下內容

使用不透明令牌配置 Resource Server
  • Java

  • Kotlin

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			.authorizeHttpRequests((authorize) -> authorize
				.anyRequest().authenticated()
			)
			.oauth2ResourceServer((oauth2) -> oauth2
				.opaqueToken(Customizer.withDefaults())
			);
		return http.build();
	}

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

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

@Configuration
@EnableWebSecurity
class SecurityConfig {

	@Bean
	fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
		http {
			authorizeHttpRequests {
				authorize(anyRequest, authenticated)
			}
			oauth2ResourceServer {
				opaqueToken { }
			}
		}

		return http.build()
	}

	@Bean
	fun opaqueTokenIntrospector(): OpaqueTokenIntrospector {
		return SpringOpaqueTokenIntrospector(
			"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 所需的全部是 JwtDecoder bean,它用於驗證簽名和解碼令牌。Spring Security 將自動使用提供的 bean 在 SecurityFilterChain 中配置保護。

以下示例使用 Spring Boot 配置屬性配置 JwtDecoder bean

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

您可以將公鑰作為類路徑資源提供(在此示例中稱為 my-public-key.pub)。

使用 Spring Boot 時,只需要這些配置。Spring Boot 提供的預設配置等同於以下內容

使用自定義 JWT 配置 Resource Server
  • Java

  • Kotlin

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			.authorizeHttpRequests((authorize) -> authorize
				.anyRequest().authenticated()
			)
			.oauth2ResourceServer((oauth2) -> oauth2
				.jwt(Customizer.withDefaults())
			);
		return http.build();
	}

	@Bean
	public JwtDecoder jwtDecoder() {
		return NimbusJwtDecoder.withPublicKey(publicKey()).build();
	}

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

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

@Configuration
@EnableWebSecurity
class SecurityConfig {

	@Bean
	fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
		http {
			authorizeHttpRequests {
				authorize(anyRequest, authenticated)
			}
			oauth2ResourceServer {
				jwt { }
			}
		}

		return http.build()
	}

	@Bean
	fun jwtDecoder(): JwtDecoder {
		return NimbusJwtDecoder.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 時,新增以下 starter:

OAuth2 Client 與 Spring Boot
  • 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 可以直接用於登入使用者(例如某些流行的社交登入提供商,它們不實現 OpenID Connect,如 GitHub 和 Facebook)。

以下示例配置應用作為 OAuth2 Client,能夠使用 OAuth2 或 OpenID Connect 登入使用者

配置 OAuth2 登入
  • Java

  • Kotlin

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.oauth2Login(Customizer.withDefaults());
		return http.build();
	}

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

@Configuration
@EnableWebSecurity
class SecurityConfig {

	@Bean
	fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
		http {
			// ...
			oauth2Login { }
		}

		return http.build()
	}

}

除了上述配置,應用還需要至少一個 ClientRegistration,這透過使用 ClientRegistrationRepository bean 進行配置。以下示例使用 Spring Boot 配置屬性配置 InMemoryClientRegistrationRepository 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 特定的元件(例如 OidcUserService)。如果缺少此 scope,Spring Security 將轉而使用 OAuth2 特定的元件(例如 DefaultOAuth2UserService)。

訪問受保護資源

對受 OAuth2 保護的第三方 API 發出請求是 OAuth2 Client 的一個核心用例。這透過授權客戶端(在 Spring Security 中由 OAuth2AuthorizedClient 類表示)並在出站請求的 Authorization 頭部中放置 Bearer 令牌來訪問受保護資源來實現。

以下示例配置應用作為 OAuth2 Client,能夠從第三方 API 請求受保護資源

配置 OAuth2 Client
  • Java

  • Kotlin

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.oauth2Client(Customizer.withDefaults());
		return http.build();
	}

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

@Configuration
@EnableWebSecurity
class SecurityConfig {

	@Bean
	fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
		http {
			// ...
			oauth2Client { }
		}

		return http.build()
	}

}

上述示例不提供使用者登入的方式。您可以使用任何其他登入機制(例如 formLogin())。請參閱下一節,瞭解結合使用 oauth2Client()oauth2Login() 的示例。

除了上述配置,應用還需要至少一個 ClientRegistration,這透過使用 ClientRegistrationRepository bean 進行配置。以下示例使用 Spring Boot 配置屬性配置 InMemoryClientRegistrationRepository 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 提供了 OAuth2AuthorizedClientManager 的實現,用於獲取可用於訪問受保護資源的訪問令牌。

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

使用 OAuth2AuthorizedClientManager 的最簡單方法是透過一個 ClientHttpRequestInterceptor,它攔截透過 RestClient 傳送的請求。當 spring-web 位於類路徑上時,RestClient 已經可用。

以下示例使用預設的 OAuth2AuthorizedClientManager 配置一個 RestClient,該 RestClient 能夠透過在每個請求的 Authorization 頭部放置 Bearer 令牌來訪問受保護資源。

使用 ClientHttpRequestInterceptor 配置 RestClient
  • Java

  • Kotlin

@Configuration
public class RestClientConfig {

	@Bean
	public RestClient restClient(OAuth2AuthorizedClientManager authorizedClientManager) {
		OAuth2ClientHttpRequestInterceptor requestInterceptor =
				new OAuth2ClientHttpRequestInterceptor(authorizedClientManager);
		return RestClient.builder()
				.requestInterceptor(requestInterceptor)
				.build();
	}

}
@Configuration
class RestClientConfig {

	@Bean
	fun restClient(authorizedClientManager: OAuth2AuthorizedClientManager): RestClient {
		val requestInterceptor = OAuth2ClientHttpRequestInterceptor(authorizedClientManager)
		return RestClient.builder()
			.requestInterceptor(requestInterceptor)
			.build()
	}

}

配置好的 RestClient 可以按照以下示例使用

使用 RestClient 訪問受保護資源
  • Java

  • Kotlin

import static org.springframework.security.oauth2.client.web.client.RequestAttributeClientRegistrationIdResolver.clientRegistrationId;

@RestController
public class MessagesController {

	private final RestClient restClient;

	public MessagesController(RestClient restClient) {
		this.restClient = restClient;
	}

	@GetMapping("/messages")
	public ResponseEntity<List<Message>> messages() {
		Message[] messages = this.restClient.get()
				.uri("https://:8090/messages")
				.attributes(clientRegistrationId("my-oauth2-client"))
				.retrieve()
				.body(Message[].class);
		return ResponseEntity.ok(Arrays.asList(messages));
	}

	public record Message(String message) {
	}

}
import org.springframework.security.oauth2.client.web.client.RequestAttributeClientRegistrationIdResolver.clientRegistrationId
import org.springframework.web.client.body

@RestController
class MessagesController(private val restClient: RestClient) {

	@GetMapping("/messages")
	fun messages(): ResponseEntity<List<Message>> {
		val messages = restClient.get()
			.uri("https://:8090/messages")
			.attributes(clientRegistrationId("my-oauth2-client"))
			.retrieve()
			.body<Array<Message>>()!!
			.toList()
		return ResponseEntity.ok(messages)
	}

	data class Message(val message: String)

}

使用 WebClient 訪問受保護資源

對受 OAuth2 保護的第三方 API 發出請求是 OAuth2 Client 的一個核心用例。這透過授權客戶端(在 Spring Security 中由 OAuth2AuthorizedClient 類表示)並在出站請求的 Authorization 頭部中放置 Bearer 令牌來訪問受保護資源來實現。

以下示例配置應用作為 OAuth2 Client,能夠從第三方 API 請求受保護資源

配置 OAuth2 Client
  • Java

  • Kotlin

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.oauth2Client(Customizer.withDefaults());
		return http.build();
	}

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

@Configuration
@EnableWebSecurity
class SecurityConfig {

	@Bean
	fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
		http {
			// ...
			oauth2Client { }
		}

		return http.build()
	}

}

上述示例不提供使用者登入的方式。您可以使用任何其他登入機制(例如 formLogin())。請參閱上一節,瞭解結合使用 oauth2Client()oauth2Login() 的示例。

除了上述配置,應用還需要至少一個 ClientRegistration,這透過使用 ClientRegistrationRepository bean 進行配置。以下示例使用 Spring Boot 配置屬性配置 InMemoryClientRegistrationRepository 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 提供了 OAuth2AuthorizedClientManager 的實現,用於獲取可用於訪問受保護資源的訪問令牌。

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

除了配置 RestClient,另一種使用 OAuth2AuthorizedClientManager 的方法是透過一個 ExchangeFilterFunction,它攔截透過 WebClient 傳送的請求。要使用 WebClient,您需要新增 spring-webflux 依賴以及一個響應式客戶端實現

新增 Spring WebFlux 依賴
  • Gradle

  • Maven

implementation 'org.springframework:spring-webflux'
implementation 'io.projectreactor.netty:reactor-netty'
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-webflux</artifactId>
</dependency>
<dependency>
	<groupId>io.projectreactor.netty</groupId>
	<artifactId>reactor-netty</artifactId>
</dependency>

以下示例使用預設的 OAuth2AuthorizedClientManager 配置一個 WebClient,該 WebClient 能夠透過在每個請求的 Authorization 頭部放置 Bearer 令牌來訪問受保護資源。

使用 ExchangeFilterFunction 配置 WebClient
  • Java

  • Kotlin

@Configuration
public class WebClientConfig {

	@Bean
	public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
		ServletOAuth2AuthorizedClientExchangeFilterFunction filter =
				new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
		return WebClient.builder()
				.apply(filter.oauth2Configuration())
				.build();
	}

}
@Configuration
class WebClientConfig {

	@Bean
	fun webClient(authorizedClientManager: OAuth2AuthorizedClientManager): WebClient {
		val filter = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager)
		return WebClient.builder()
			.apply(filter.oauth2Configuration())
			.build()
	}

}

配置好的 WebClient 可以按照以下示例使用

使用 WebClient 訪問受保護資源
  • Java

  • Kotlin

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

@RestController
public class MessagesController {

	private final WebClient webClient;

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

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

	public record Message(String message) {
	}

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

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

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

	data class Message(val message: String)

}

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

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

本節將 使用 OAuth2 登入使用者訪問受保護資源 合併到一個配置中。還存在其他高階場景,例如為一個 ClientRegistration 配置登入,為另一個配置訪問受保護資源。所有這些場景都將使用相同的基本配置。

以下示例配置應用作為 OAuth2 Client,能夠登入使用者 從第三方 API 請求受保護資源

配置 OAuth2 登入和 OAuth2 Client
  • Java

  • Kotlin

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.oauth2Login(Customizer.withDefaults())
			.oauth2Client(Customizer.withDefaults());
		return http.build();
	}

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

@Configuration
@EnableWebSecurity
class SecurityConfig {

	@Bean
	fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
		http {
			// ...
			oauth2Login { }
			oauth2Client { }
		}

		return http.build()
	}

}

除了上述配置,應用還需要至少一個 ClientRegistration,這透過使用 ClientRegistrationRepository bean 進行配置。以下示例使用 Spring Boot 配置屬性配置 InMemoryClientRegistrationRepository 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 提供了 OAuth2AuthorizedClientManager 的實現,用於獲取可用於訪問受保護資源的訪問令牌。

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

使用 OAuth2AuthorizedClientManager 的最簡單方法是透過一個 ClientHttpRequestInterceptor,它攔截透過 RestClient 傳送的請求。當 spring-web 位於類路徑上時,RestClient 已經可用。

以下示例使用預設的 OAuth2AuthorizedClientManager 配置一個 RestClient,該 RestClient 能夠透過在每個請求的 Authorization 頭部放置 Bearer 令牌來訪問受保護資源。

使用 ClientHttpRequestInterceptor 配置 RestClient
  • Java

  • Kotlin

@Configuration
public class RestClientConfig {

	@Bean
	public RestClient restClient(OAuth2AuthorizedClientManager authorizedClientManager) {
		OAuth2ClientHttpRequestInterceptor requestInterceptor =
				new OAuth2ClientHttpRequestInterceptor(authorizedClientManager);
		requestInterceptor.setClientRegistrationIdResolver(clientRegistrationIdResolver());

		return RestClient.builder()
				.requestInterceptor(requestInterceptor)
				.build();
	}

	private static ClientRegistrationIdResolver clientRegistrationIdResolver() {
		return (request) -> {
			Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
			return (authentication instanceof OAuth2AuthenticationToken principal)
				? principal.getAuthorizedClientRegistrationId()
				: null;
		};
	}

}
@Configuration
class RestClientConfig {

	@Bean
	fun restClient(authorizedClientManager: OAuth2AuthorizedClientManager): RestClient {
		val requestInterceptor = OAuth2ClientHttpRequestInterceptor(authorizedClientManager)
		requestInterceptor.setClientRegistrationIdResolver(clientRegistrationIdResolver())

		return RestClient.builder()
			.requestInterceptor(requestInterceptor)
			.build()
	}

	private fun clientRegistrationIdResolver(): OAuth2ClientHttpRequestInterceptor.ClientRegistrationIdResolver {
		return OAuth2ClientHttpRequestInterceptor.ClientRegistrationIdResolver { request ->
			val authentication = SecurityContextHolder.getContext().authentication
			if (authentication is OAuth2AuthenticationToken) {
				authentication.authorizedClientRegistrationId
			} else {
				null
			}
		}
	}

}

配置好的 RestClient 可以按照以下示例使用

使用 RestClient 訪問受保護資源(當前使用者)
  • Java

  • Kotlin

@RestController
public class MessagesController {

	private final RestClient restClient;

	public MessagesController(RestClient restClient) {
		this.restClient = restClient;
	}

	@GetMapping("/messages")
	public ResponseEntity<List<Message>> messages() {
		Message[] messages = this.restClient.get()
				.uri("https://:8090/messages")
				.retrieve()
				.body(Message[].class);
		return ResponseEntity.ok(Arrays.asList(messages));
	}

	public record Message(String message) {
	}

}
import org.springframework.web.client.body

@RestController
class MessagesController(private val restClient: RestClient) {

	@GetMapping("/messages")
	fun messages(): ResponseEntity<List<Message>> {
		val messages = restClient.get()
			.uri("https://:8090/messages")
			.retrieve()
			.body<Array<Message>>()!!
			.toList()
		return ResponseEntity.ok(messages)
	}

	data class Message(val message: String)

}

上一個示例不同,請注意我們無需告知 Spring Security 我們希望使用的 clientRegistrationId。這是因為它可以從當前登入的使用者中派生出來。

使用客戶端憑證授權型別

本節重點介紹客戶端憑證授權型別的其他注意事項。有關所有授權型別的通用設定和用法,請參閱訪問受保護資源

遵循 客戶端憑證授權 允許客戶端代表自身獲取 access_token。客戶端憑證授權是一種不涉及資源所有者(即使用者)的簡單流程。

值得注意的是,客戶端憑證授權的典型用法意味著任何請求(或使用者)都可能獲取訪問令牌並向資源伺服器發出受保護資源請求。在設計應用時務必謹慎,以確保使用者不能發出未經授權的請求,因為每個請求都將能夠獲取訪問令牌。

在使用者可以登入的 Web 應用中獲取訪問令牌時,Spring Security 的預設行為是按使用者獲取訪問令牌。

預設情況下,訪問令牌的作用域限定在當前使用者的 principal name,這意味著每個使用者都將收到一個唯一的訪問令牌。

使用客戶端憑證授權的客戶端通常要求訪問令牌的作用域限定在應用級別,而不是針對單個使用者,這樣每個應用只有一個訪問令牌。為了將訪問令牌的作用域限定在應用級別,您需要設定一個用於解析自定義 principal name 的策略。以下示例透過使用 RequestAttributePrincipalResolver 配置 RestClient 來實現這一點

client_credentials 配置 RestClient
  • Java

  • Kotlin

@Configuration
public class RestClientConfig {

	@Bean
	public RestClient restClient(OAuth2AuthorizedClientManager authorizedClientManager) {
		OAuth2ClientHttpRequestInterceptor requestInterceptor =
				new OAuth2ClientHttpRequestInterceptor(authorizedClientManager);
		requestInterceptor.setPrincipalResolver(new RequestAttributePrincipalResolver());
		return RestClient.builder()
				.requestInterceptor(requestInterceptor)
				.build();
	}

}
@Configuration
class RestClientConfig {

	@Bean
	fun restClient(authorizedClientManager: OAuth2AuthorizedClientManager): RestClient {
		val requestInterceptor = OAuth2ClientHttpRequestInterceptor(authorizedClientManager)
		requestInterceptor.setPrincipalResolver(RequestAttributePrincipalResolver())
		return RestClient.builder()
			.requestInterceptor(requestInterceptor)
			.build()
	}

}

透過上述配置,可以為每個請求指定一個 principal name。以下示例演示瞭如何透過指定 principal name 將訪問令牌的作用域限定在應用級別

將訪問令牌作用域限定在應用級別
  • Java

  • Kotlin

import static org.springframework.security.oauth2.client.web.client.RequestAttributeClientRegistrationIdResolver.clientRegistrationId;
import static org.springframework.security.oauth2.client.web.client.RequestAttributePrincipalResolver.principal;

@RestController
public class MessagesController {

	private final RestClient restClient;

	public MessagesController(RestClient restClient) {
		this.restClient = restClient;
	}

	@GetMapping("/messages")
	public ResponseEntity<List<Message>> messages() {
		Message[] messages = this.restClient.get()
				.uri("https://:8090/messages")
				.attributes(clientRegistrationId("my-oauth2-client"))
				.attributes(principal("my-application"))
				.retrieve()
				.body(Message[].class);
		return ResponseEntity.ok(Arrays.asList(messages));
	}

	public record Message(String message) {
	}

}
import org.springframework.security.oauth2.client.web.client.RequestAttributeClientRegistrationIdResolver.clientRegistrationId
import org.springframework.security.oauth2.client.web.client.RequestAttributePrincipalResolver.principal
import org.springframework.web.client.body

@RestController
class MessagesController(private val restClient: RestClient) {

	@GetMapping("/messages")
	fun messages(): ResponseEntity<List<Message>> {
		val messages = restClient.get()
			.uri("https://:8090/messages")
			.attributes(clientRegistrationId("my-oauth2-client"))
			.attributes(principal("my-application"))
			.retrieve()
			.body<Array<Message>>()!!
			.toList()
		return ResponseEntity.ok(messages)
	}

	data class Message(val message: String)

}

如上例所示,透過屬性指定 principal name 時,將只有一個訪問令牌,並且該令牌將用於所有請求。

啟用擴充套件授權型別

啟用和/或配置擴充套件授權型別是一個常見的用例。例如,Spring Security 支援 jwt-bearertoken-exchange 授權型別,但預設不啟用它們,因為它們不是核心 OAuth 2.0 規範的一部分。

使用 Spring Security 6.2 及更高版本,我們可以簡單地釋出一個或多個 OAuth2AuthorizedClientProvider 的 bean,它們將被自動識別。以下示例僅啟用 jwt-bearer 授權型別

啟用 jwt-bearer 授權型別
  • Java

  • Kotlin

@Configuration
public class SecurityConfig {

	@Bean
	public OAuth2AuthorizedClientProvider jwtBearer() {
		return new JwtBearerOAuth2AuthorizedClientProvider();
	}

}
@Configuration
class SecurityConfig {

	@Bean
	fun jwtBearer(): OAuth2AuthorizedClientProvider {
		return JwtBearerOAuth2AuthorizedClientProvider()
	}

}

當沒有提供 OAuth2AuthorizedClientManager bean 時,Spring Security 將自動釋出一個預設的。

任何自定義的 OAuth2AuthorizedClientProvider bean 也將被識別,並在預設授權型別之後應用於提供的 OAuth2AuthorizedClientManager

為了在 Spring Security 6.2 之前實現上述配置,我們必須自己釋出此 bean,並確保也重新啟用預設授權型別。為了理解幕後配置的內容,配置可能如下所示

啟用 jwt-bearer 授權型別(6.2 之前)
  • Java

  • Kotlin

@Configuration
public class SecurityConfig {

	@Bean
	public OAuth2AuthorizedClientManager authorizedClientManager(
			ClientRegistrationRepository clientRegistrationRepository,
			OAuth2AuthorizedClientRepository authorizedClientRepository) {

		OAuth2AuthorizedClientProvider authorizedClientProvider =
			OAuth2AuthorizedClientProviderBuilder.builder()
				.authorizationCode()
				.refreshToken()
				.clientCredentials()
				.password()
				.provider(new JwtBearerOAuth2AuthorizedClientProvider())
				.build();

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

		return authorizedClientManager;
	}

}
@Configuration
class SecurityConfig {

	@Bean
	fun authorizedClientManager(
		clientRegistrationRepository: ClientRegistrationRepository,
		authorizedClientRepository: OAuth2AuthorizedClientRepository
	): OAuth2AuthorizedClientManager {
		val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
			.authorizationCode()
			.refreshToken()
			.clientCredentials()
			.password()
			.provider(JwtBearerOAuth2AuthorizedClientProvider())
			.build()

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

		return authorizedClientManager
	}

}

自定義現有授權型別

透過釋出 bean 來啟用擴充套件授權型別 的能力也為自定義現有授權型別提供了機會,而無需重新定義預設值。例如,如果我們要自定義 OAuth2AuthorizedClientProvider 的時鐘偏差以用於 client_credentials 授權,我們可以簡單地釋出一個 bean,如下所示

自定義客戶端憑證授權型別
  • Java

  • Kotlin

@Configuration
public class SecurityConfig {

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

		return authorizedClientProvider;
	}

}
@Configuration
class SecurityConfig {

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

}

自定義令牌請求引數

在獲取訪問令牌時自定義請求引數的需求相當常見。例如,假設我們想向令牌請求新增一個自定義的 audience 引數,因為提供商對 authorization_code 授權需要此引數。

使用 Spring Security 6.2 及更高版本,我們可以簡單地釋出一個型別為 OAuth2AccessTokenResponseClient 且泛型型別為 OAuth2AuthorizationCodeGrantRequest 的 bean,Spring Security 將使用它來配置 OAuth2 Client 元件。

以下示例在不使用 DSL 的情況下自定義 authorization_code 授權的令牌請求引數

自定義授權碼授權的令牌請求引數
  • Java

  • Kotlin

@Configuration
public class SecurityConfig {

	@Bean
	public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> authorizationCodeAccessTokenResponseClient() {
		OAuth2AuthorizationCodeGrantRequestEntityConverter requestEntityConverter =
			new OAuth2AuthorizationCodeGrantRequestEntityConverter();
		requestEntityConverter.addParametersConverter(parametersConverter());

		DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient =
			new DefaultAuthorizationCodeTokenResponseClient();
		accessTokenResponseClient.setRequestEntityConverter(requestEntityConverter);

		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(): OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
		val requestEntityConverter = OAuth2AuthorizationCodeGrantRequestEntityConverter()
		requestEntityConverter.addParametersConverter(parametersConverter())

		val accessTokenResponseClient = DefaultAuthorizationCodeTokenResponseClient()
		accessTokenResponseClient.setRequestEntityConverter(requestEntityConverter)

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

}

請注意,在這種情況下,我們不需要自定義 SecurityFilterChain bean,並且可以保留預設設定。如果使用 Spring Boot 且沒有其他自定義,我們實際上可以完全省略 SecurityFilterChain bean。

在 Spring Security 6.2 之前,我們必須確保此自定義應用於 OAuth2 Login(如果使用此功能)和 OAuth2 Client 元件,使用 Spring Security DSL。為了理解幕後配置的內容,配置可能如下所示

自定義授權碼授權的令牌請求引數(6.2 之前)
  • Java

  • Kotlin

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		OAuth2AuthorizationCodeGrantRequestEntityConverter requestEntityConverter =
			new OAuth2AuthorizationCodeGrantRequestEntityConverter();
		requestEntityConverter.addParametersConverter(parametersConverter());

		DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient =
			new DefaultAuthorizationCodeTokenResponseClient();
		accessTokenResponseClient.setRequestEntityConverter(requestEntityConverter);

		http
			.authorizeHttpRequests((authorize) -> authorize
				.anyRequest().authenticated()
			)
			.oauth2Login((oauth2Login) -> oauth2Login
				.tokenEndpoint((tokenEndpoint) -> tokenEndpoint
					.accessTokenResponseClient(accessTokenResponseClient)
				)
			)
			.oauth2Client((oauth2Client) -> oauth2Client
				.authorizationCodeGrant((authorizationCode) -> authorizationCode
					.accessTokenResponseClient(accessTokenResponseClient)
				)
			);

		return http.build();
	}

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

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

@Configuration
@EnableWebSecurity
class SecurityConfig {

	@Bean
	fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
		val requestEntityConverter = OAuth2AuthorizationCodeGrantRequestEntityConverter()
		requestEntityConverter.addParametersConverter(parametersConverter())

		val tokenResponseClient = DefaultAuthorizationCodeTokenResponseClient()
		tokenResponseClient.setRequestEntityConverter(requestEntityConverter)

		http {
			authorizeHttpRequests {
				authorize(anyRequest, authenticated)
			}
			oauth2Login {
				tokenEndpoint {
					accessTokenResponseClient = tokenResponseClient
				}
			}
			oauth2Client {
				authorizationCodeGrant {
					accessTokenResponseClient = tokenResponseClient
				}
			}
		}

		return http.build()
	}

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

}

對於其他授權型別,我們可以釋出額外的 OAuth2AccessTokenResponseClient bean 來覆蓋預設設定。例如,要自定義 client_credentials 授權的令牌請求,我們可以釋出以下 bean

自定義客戶端憑證授權的令牌請求引數
  • Java

  • Kotlin

@Configuration
public class SecurityConfig {

	@Bean
	public OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsAccessTokenResponseClient() {
		OAuth2ClientCredentialsGrantRequestEntityConverter requestEntityConverter =
			new OAuth2ClientCredentialsGrantRequestEntityConverter();
		requestEntityConverter.addParametersConverter(parametersConverter());

		DefaultClientCredentialsTokenResponseClient accessTokenResponseClient =
				new DefaultClientCredentialsTokenResponseClient();
		accessTokenResponseClient.setRequestEntityConverter(requestEntityConverter);

		return accessTokenResponseClient;
	}

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

}
@Configuration
class SecurityConfig {

	@Bean
	fun clientCredentialsAccessTokenResponseClient(): OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> {
		val requestEntityConverter = OAuth2ClientCredentialsGrantRequestEntityConverter()
		requestEntityConverter.addParametersConverter(parametersConverter())

		val accessTokenResponseClient = DefaultClientCredentialsTokenResponseClient()
		accessTokenResponseClient.setRequestEntityConverter(requestEntityConverter)

		return accessTokenResponseClient
	}

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

}

Spring Security 自動解析以下 OAuth2AccessTokenResponseClient bean 的泛型型別

  • OAuth2AuthorizationCodeGrantRequest(參見 DefaultAuthorizationCodeTokenResponseClient

  • OAuth2RefreshTokenGrantRequest(參見 DefaultRefreshTokenTokenResponseClient

  • OAuth2ClientCredentialsGrantRequest(參見 DefaultClientCredentialsTokenResponseClient

  • OAuth2PasswordGrantRequest(參見 DefaultPasswordTokenResponseClient

  • JwtBearerGrantRequest(參見 DefaultJwtBearerTokenResponseClient

  • TokenExchangeGrantRequest(參見 DefaultTokenExchangeTokenResponseClient

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

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

自定義 OAuth2 Client 元件使用的 RestOperations

另一個常見的用例是需要自定義獲取訪問令牌時使用的 RestOperations。我們可能需要這樣做來自定義響應的處理(透過自定義的 HttpMessageConverter)或為公司網路應用代理設定(透過自定義的 ClientHttpRequestFactory)。

使用 Spring Security 6.2 及更高版本,我們可以簡單地釋出型別為 OAuth2AccessTokenResponseClient 的 bean,Spring Security 將為我們配置併發佈一個 OAuth2AuthorizedClientManager bean。

以下示例為所有支援的授權型別自定義 RestOperations

自定義 OAuth2 Client 的 RestOperations
  • Java

  • Kotlin

@Configuration
public class SecurityConfig {

	@Bean
	public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> authorizationCodeAccessTokenResponseClient() {
		DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient =
			new DefaultAuthorizationCodeTokenResponseClient();
		accessTokenResponseClient.setRestOperations(restTemplate());

		return accessTokenResponseClient;
	}

	@Bean
	public OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenAccessTokenResponseClient() {
		DefaultRefreshTokenTokenResponseClient accessTokenResponseClient =
			new DefaultRefreshTokenTokenResponseClient();
		accessTokenResponseClient.setRestOperations(restTemplate());

		return accessTokenResponseClient;
	}

	@Bean
	public OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsAccessTokenResponseClient() {
		DefaultClientCredentialsTokenResponseClient accessTokenResponseClient =
			new DefaultClientCredentialsTokenResponseClient();
		accessTokenResponseClient.setRestOperations(restTemplate());

		return accessTokenResponseClient;
	}

	@Bean
	public OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> passwordAccessTokenResponseClient() {
		DefaultPasswordTokenResponseClient accessTokenResponseClient =
			new DefaultPasswordTokenResponseClient();
		accessTokenResponseClient.setRestOperations(restTemplate());

		return accessTokenResponseClient;
	}

	@Bean
	public OAuth2AccessTokenResponseClient<JwtBearerGrantRequest> jwtBearerAccessTokenResponseClient() {
		DefaultJwtBearerTokenResponseClient accessTokenResponseClient =
			new DefaultJwtBearerTokenResponseClient();
		accessTokenResponseClient.setRestOperations(restTemplate());

		return accessTokenResponseClient;
	}

	@Bean
	public OAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> tokenExchangeAccessTokenResponseClient() {
		DefaultTokenExchangeTokenResponseClient accessTokenResponseClient =
			new DefaultTokenExchangeTokenResponseClient();
		accessTokenResponseClient.setRestOperations(restTemplate());

		return accessTokenResponseClient;
	}

	@Bean
	public RestTemplate restTemplate() {
		// ...
	}

}
@Configuration
class SecurityConfig {

	@Bean
	fun authorizationCodeAccessTokenResponseClient(): OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
		val accessTokenResponseClient = DefaultAuthorizationCodeTokenResponseClient()
		accessTokenResponseClient.setRestOperations(restTemplate())

		return accessTokenResponseClient
	}

	@Bean
	fun refreshTokenAccessTokenResponseClient(): OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> {
		val accessTokenResponseClient = DefaultRefreshTokenTokenResponseClient()
		accessTokenResponseClient.setRestOperations(restTemplate())

		return accessTokenResponseClient
	}

	@Bean
	fun clientCredentialsAccessTokenResponseClient(): OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> {
		val accessTokenResponseClient = DefaultClientCredentialsTokenResponseClient()
		accessTokenResponseClient.setRestOperations(restTemplate())

		return accessTokenResponseClient
	}

	@Bean
	fun passwordAccessTokenResponseClient(): OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> {
		val accessTokenResponseClient = DefaultPasswordTokenResponseClient()
		accessTokenResponseClient.setRestOperations(restTemplate())

		return accessTokenResponseClient
	}

	@Bean
	fun jwtBearerAccessTokenResponseClient(): OAuth2AccessTokenResponseClient<JwtBearerGrantRequest> {
		val accessTokenResponseClient = DefaultJwtBearerTokenResponseClient()
		accessTokenResponseClient.setRestOperations(restTemplate())

		return accessTokenResponseClient
	}

	@Bean
	fun tokenExchangeAccessTokenResponseClient(): OAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> {
		val accessTokenResponseClient = DefaultTokenExchangeTokenResponseClient()
		accessTokenResponseClient.setRestOperations(restTemplate())

		return accessTokenResponseClient
	}

	@Bean
	fun restTemplate(): RestTemplate {
		// ...
	}

}

當沒有提供 OAuth2AuthorizedClientManager bean 時,Spring Security 將自動釋出一個預設的。

請注意,在這種情況下,我們不需要自定義 SecurityFilterChain bean,並且可以保留預設設定。如果使用 Spring Boot 且沒有其他自定義,我們實際上可以完全省略 SecurityFilterChain bean。

在 Spring Security 6.2 之前,我們必須確保此自定義應用於 OAuth2 Login(如果使用此功能)和 OAuth2 Client 元件。我們必須同時使用 Spring Security DSL(用於 authorization_code 授權)併為其他授權型別釋出型別為 OAuth2AuthorizedClientManager 的 bean。為了理解幕後配置的內容,配置可能如下所示

自定義 OAuth2 Client 的 RestOperations(6.2 之前)
  • Java

  • Kotlin

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient =
			new DefaultAuthorizationCodeTokenResponseClient();
		accessTokenResponseClient.setRestOperations(restTemplate());

		http
			// ...
			.oauth2Login((oauth2Login) -> oauth2Login
				.tokenEndpoint((tokenEndpoint) -> tokenEndpoint
					.accessTokenResponseClient(accessTokenResponseClient)
				)
			)
			.oauth2Client((oauth2Client) -> oauth2Client
				.authorizationCodeGrant((authorizationCode) -> authorizationCode
					.accessTokenResponseClient(accessTokenResponseClient)
				)
			);

		return http.build();
	}

	@Bean
	public OAuth2AuthorizedClientManager authorizedClientManager(
			ClientRegistrationRepository clientRegistrationRepository,
			OAuth2AuthorizedClientRepository authorizedClientRepository) {

		DefaultRefreshTokenTokenResponseClient refreshTokenAccessTokenResponseClient =
			new DefaultRefreshTokenTokenResponseClient();
		refreshTokenAccessTokenResponseClient.setRestOperations(restTemplate());

		DefaultClientCredentialsTokenResponseClient clientCredentialsAccessTokenResponseClient =
			new DefaultClientCredentialsTokenResponseClient();
		clientCredentialsAccessTokenResponseClient.setRestOperations(restTemplate());

		DefaultPasswordTokenResponseClient passwordAccessTokenResponseClient =
			new DefaultPasswordTokenResponseClient();
		passwordAccessTokenResponseClient.setRestOperations(restTemplate());

		DefaultJwtBearerTokenResponseClient jwtBearerAccessTokenResponseClient =
			new DefaultJwtBearerTokenResponseClient();
		jwtBearerAccessTokenResponseClient.setRestOperations(restTemplate());

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

		DefaultTokenExchangeTokenResponseClient tokenExchangeAccessTokenResponseClient =
			new DefaultTokenExchangeTokenResponseClient();
		tokenExchangeAccessTokenResponseClient.setRestOperations(restTemplate());

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

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

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

		return authorizedClientManager;
	}

	@Bean
	public RestTemplate restTemplate() {
		// ...
	}

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

@Configuration
@EnableWebSecurity
class SecurityConfig {

	@Bean
	fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
		val tokenResponseClient = DefaultAuthorizationCodeTokenResponseClient()
		tokenResponseClient.setRestOperations(restTemplate())

		http {
			// ...
			oauth2Login {
				tokenEndpoint {
					accessTokenResponseClient = tokenResponseClient
				}
			}
			oauth2Client {
				authorizationCodeGrant {
					accessTokenResponseClient = tokenResponseClient
				}
			}
		}

		return http.build()
	}

	@Bean
	fun authorizedClientManager(
		clientRegistrationRepository: ClientRegistrationRepository?,
		authorizedClientRepository: OAuth2AuthorizedClientRepository?
	): OAuth2AuthorizedClientManager {
		val refreshTokenAccessTokenResponseClient = DefaultRefreshTokenTokenResponseClient()
		refreshTokenAccessTokenResponseClient.setRestOperations(restTemplate())

		val clientCredentialsAccessTokenResponseClient = DefaultClientCredentialsTokenResponseClient()
		clientCredentialsAccessTokenResponseClient.setRestOperations(restTemplate())

		val passwordAccessTokenResponseClient = DefaultPasswordTokenResponseClient()
		passwordAccessTokenResponseClient.setRestOperations(restTemplate())

		val jwtBearerAccessTokenResponseClient = DefaultJwtBearerTokenResponseClient()
		jwtBearerAccessTokenResponseClient.setRestOperations(restTemplate())

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

		val tokenExchangeAccessTokenResponseClient = DefaultTokenExchangeTokenResponseClient()
		tokenExchangeAccessTokenResponseClient.setRestOperations(restTemplate())

		val tokenExchangeAuthorizedClientProvider = TokenExchangeOAuth2AuthorizedClientProvider()
		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 = DefaultOAuth2AuthorizedClientManager(
			clientRegistrationRepository, authorizedClientRepository
		)
		authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)

		return authorizedClientManager
	}

	@Bean
	fun restTemplate(): RestTemplate {
		// ...
	}

}

進一步閱讀

前面的章節介紹了 Spring Security 對 OAuth2 的支援以及常見場景的示例。您可以在參考文件的以下章節中閱讀有關 OAuth2 Client 和 Resource Server 的更多內容