OAuth2

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

總覽

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

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

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

OAuth2 中的資源伺服器用戶端角色通常由一個或多個伺服器端應用程式表示。此外,授權伺服器角色可以由一個或多個協力廠商表示(例如在組織內集中身分管理和/或驗證的情況下)-或-它可以由一個應用程式表示(例如 Spring Authorization Server 的情況)。

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

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

OAuth2 資源伺服器

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

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

搭配 Spring Boot 的 OAuth2 用戶端
  • Gradle

  • Maven

implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

若未使用 Spring Boot,如需其他選項,請參閱 取得 Spring Security

請考慮以下 OAuth2 資源伺服器的使用案例

使用 OAuth2 存取權杖保護存取

使用 OAuth2 存取權杖保護對 API 的存取非常常見。在大多數情況下,Spring Security 僅需最少的組態即可使用 OAuth2 保護應用程式的安全。

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

JWT 支援

以下範例使用 Spring Boot 組態屬性組態 JwtDecoder bean

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

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

使用 JWT 組態資源伺服器
  • 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 提供的預設配置相當於以下內容

使用不透明權杖組態資源伺服器
  • 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 資源伺服器支援可用於任何類型的 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 組態資源伺服器
  • 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 用戶端

本節包含 OAuth2 用戶端功能的摘要和範例。如需完整的參考文件,請參閱 OAuth 2.0 用戶端OAuth 2.0 登入

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

搭配 Spring Boot 的 OAuth2 用戶端
  • Gradle

  • Maven

implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

若未使用 Spring Boot,如需其他選項,請參閱 取得 Spring Security

請考慮以下 OAuth2 用戶端的使用案例

使用 OAuth2 讓使用者登入

讓使用者透過 OAuth2 登入非常常見。OpenID Connect 1.0 提供一個名為 id_token 的特殊權杖,旨在為 OAuth2 用戶端提供執行使用者身分驗證和讓使用者登入的能力。在某些情況下,OAuth2 可以直接用於讓使用者登入(例如,流行的社交登入提供者,例如 GitHub 和 Facebook,它們未實作 OpenID Connect)。

以下範例組態應用程式作為 OAuth2 用戶端,能夠使用 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 範圍的存在表示應使用 OpenID Connect 1.0。這指示 Spring Security 在請求處理期間使用 OIDC 特定的元件(例如 OidcUserService)。如果沒有此範圍,Spring Security 將改用 OAuth2 特定的元件(例如 DefaultOAuth2UserService)。

存取受保護的資源

向受 OAuth2 保護的協力廠商 API 發出請求是 OAuth2 用戶端的核心使用案例。這是透過授權用戶端(由 Spring Security 中的 OAuth2AuthorizedClient 類別表示)並透過將 Bearer 權杖放在輸出請求的 Authorization 標頭中來存取受保護的資源來完成的。

以下範例組態應用程式作為 OAuth2 用戶端,能夠從協力廠商 API 請求受保護的資源

組態 OAuth2 用戶端
  • 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 用戶端功能之外,您還需要決定將如何存取受保護的資源,並據此組態您的應用程式。Spring Security 提供 OAuth2AuthorizedClientManager 的實作,用於取得可用於存取受保護資源的存取權杖。

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

使用 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,使其能夠透過將 Bearer 權杖放在每個請求的 Authorization 標頭中來存取受保護的資源

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

	data class Message(val message: String)

}

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

當使用者透過 OAuth2 或 OpenID Connect 登入時,授權伺服器可能會提供可用於直接存取受保護資源的存取權杖。這很方便,因為它只需要為這兩個使用案例同時組態單一 ClientRegistration

本節將 使用 OAuth2 讓使用者登入存取受保護的資源 合併到單一組態中。還存在其他進階情境,例如組態一個用於登入的 ClientRegistration 和另一個用於存取受保護資源的 ClientRegistration。所有這類情境都將使用相同的基本組態。

以下範例組態應用程式作為 OAuth2 用戶端,能夠讓使用者登入從協力廠商 API 請求受保護的資源

組態 OAuth2 登入和 OAuth2 用戶端
  • 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 屬性組態的內容,它將標準範圍 openidprofile 與自訂範圍 message.readmessage.write 結合在一起。

除了組態 Spring Security 以支援 OAuth2 用戶端功能之外,您還需要決定將如何存取受保護的資源,並據此組態您的應用程式。Spring Security 提供 OAuth2AuthorizedClientManager 的實作,用於取得可用於存取受保護資源的存取權杖。

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

使用 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,使其能夠透過將 Bearer 權杖放在每個請求的 Authorization 標頭中來存取受保護的資源

使用 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

@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://127.0.0.1:8090/messages")
				.retrieve()
				.toEntityList(Message.class)
				.block();
	}

	public record Message(String message) {
	}

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

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

	data class Message(val message: String)

}

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

啟用擴充授權類型

常見的使用案例包括啟用和/或組態擴充授權類型。例如,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 尚未提供時,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 來啟用擴充授權類型的能力也提供了自訂現有授權類型的機會,而無需重新定義預設值。例如,如果我們想要自訂 client_credentials 授權的 OAuth2AuthorizedClientProvider 的時鐘偏移,我們可以簡單地發佈如下所示的 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 用戶端元件。

以下範例自訂 authorization_code 授權的權杖請求參數,而不使用 DSL

自訂授權碼授權的權杖請求參數
  • 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 登入(如果我們正在使用此功能)和使用 Spring Security DSL 的 OAuth2 用戶端元件。若要瞭解幕後組態的內容,以下是組態可能看起來的樣子

自訂授權碼授權的權杖請求參數(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 用戶端元件使用的 RestOperations

另一個常見的使用案例是需要自訂取得存取權杖時使用的 RestOperations。我們可能需要這樣做來自訂回應的處理(透過自訂 HttpMessageConverter)或針對企業網路套用 Proxy 設定(透過自訂的 ClientHttpRequestFactory)。

在 Spring Security 6.2 和更新版本中,我們可以簡單地發佈類型為 OAuth2AccessTokenResponseClient 的 bean,Spring Security 將會為我們組態並發佈 OAuth2AuthorizedClientManager bean。

以下範例自訂所有支援的授權類型的 RestOperations

自訂 OAuth2 用戶端的 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 尚未提供時,Spring Security 將會自動發佈一個。

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

在 Spring Security 6.2 之前,我們必須確保此自訂套用於 OAuth2 登入(如果我們正在使用此功能)和 OAuth2 用戶端元件。我們必須同時使用 Spring Security DSL(針對 authorization_code 授權)並發佈類型為 OAuth2AuthorizedClientManager 的 bean,用於其他授權類型。若要瞭解幕後組態的內容,以下是組態可能看起來的樣子

自訂 OAuth2 用戶端的 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 用戶端和資源伺服器的更多資訊