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
-
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 保護 API 的存取權限 (授權伺服器提供 JWT 或不透明的 access token)
-
我想使用 JWT 保護 API 的存取權限 (自訂 token)
使用 OAuth2 Access Token 保護存取權限
使用 OAuth2 access token 保護 API 的存取權限非常常見。在大多數情況下,Spring Security 只需要最少的設定即可使用 OAuth2 保護應用程式。
Spring Security 支援兩種 Bearer
token 類型,每種類型都使用不同的元件進行驗證
-
JWT 支援使用
ReactiveJwtDecoder
bean 來驗證簽章和解碼 token -
不透明 token 支援使用
ReactiveOpaqueTokenIntrospector
bean 來內省 token
JWT 支援
以下範例使用 Spring Boot 設定屬性設定 ReactiveJwtDecoder
bean
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://my-auth-server.com
使用 Spring Boot 時,這就是所有需要的設定。Spring Boot 提供的預設配置相當於以下設定
-
Java
-
Kotlin
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange((authorize) -> authorize
.anyExchange().authenticated()
)
.oauth2ResourceServer((oauth2) -> oauth2
.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 提供的預設配置相當於以下設定
-
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 資源提供 (在本範例中稱為 |
使用 Spring Boot 時,這就是所有需要的設定。Spring Boot 提供的預設配置相當於以下設定
-
Java
-
Kotlin
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange((authorize) -> authorize
.anyExchange().authenticated()
)
.oauth2ResourceServer((oauth2) -> oauth2
.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 提供了 |
OAuth2 Client
本節包含 OAuth2 Client 功能的摘要以及範例。如需完整的參考文件,請參閱 OAuth 2.0 Client 和 OAuth 2.0 登入。 |
若要開始使用,請將 spring-security-oauth2-client
相依性新增至您的專案。使用 Spring Boot 時,請新增以下 starter
-
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 讓使用者登入
-
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
透過上述設定,應用程式現在支援兩個額外的端點
-
登入端點 (例如
/oauth2/authorization/my-oidc-client
) 用於啟動登入並執行重新導向至第三方授權伺服器。 -
重新導向端點 (例如
/login/oauth2/code/my-oidc-client
) 由授權伺服器用於重新導向回 client 應用程式,並且將包含一個code
參數,用於透過 access token 請求取得id_token
和/或access_token
。
上述設定中存在 |
存取受保護的資源
向受 OAuth2 保護的第三方 API 發出請求是 OAuth2 Client 的核心使用案例。這是透過授權 client (由 Spring Security 中的 OAuth2AuthorizedClient
類別表示),並透過在輸出請求的 Authorization
標頭中放置 Bearer token 來存取受保護的資源來完成的。
以下範例設定應用程式作為 OAuth2 Client,能夠向第三方 API 請求受保護的資源
-
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 { }
}
}
}
上述範例未提供讓使用者登入的方式。您可以使用任何其他登入機制 (例如 |
除了上述設定之外,應用程式還需要至少一個 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
最簡單的方式是透過 ExchangeFilterFunction
,它會攔截透過 WebClient
的請求。
以下範例使用預設的 ReactiveOAuth2AuthorizedClientManager
設定 WebClient
,使其能夠透過在每個請求的 Authorization
標頭中放置 Bearer token 來存取受保護的資源
-
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
可以像以下範例中那樣使用
-
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 讓使用者登入」和「存取受保護的資源」結合到單一設定中。還存在其他進階情境,例如為登入設定一個 |
以下範例設定應用程式作為 OAuth2 Client,能夠讓使用者登入 *並* 向第三方 API 請求受保護的資源
-
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 讓使用者登入」、「存取受保護的資源」) 與此範例之間的主要區別在於透過 |
除了設定 Spring Security 以支援 OAuth2 Client 功能外,您還需要決定如何存取受保護的資源,並據此設定您的應用程式。Spring Security 提供了 ReactiveOAuth2AuthorizedClientManager
的實作,用於取得可用於存取受保護資源的 access token。
當不存在 |
使用 ReactiveOAuth2AuthorizedClientManager
最簡單的方式是透過 ExchangeFilterFunction
,它會攔截透過 WebClient
的請求。
以下範例使用預設的 ReactiveOAuth2AuthorizedClientManager
設定 WebClient
,使其能夠透過在每個請求的 Authorization
標頭中放置 Bearer token 來存取受保護的資源
-
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
可以像以下範例中那樣使用
-
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 我們想要使用的 |
啟用擴充授權類型
常見的使用案例包括啟用和/或設定擴充授權類型。例如,Spring Security 支援 jwt-bearer
和 token-exchange
授權類型,但預設情況下不啟用它們,因為它們不是核心 OAuth 2.0 規範的一部分。
在 Spring Security 6.3 及更新版本中,我們只需發布一個或多個 ReactiveOAuth2AuthorizedClientProvider
的 bean,它們就會自動被選取。以下範例僅啟用 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。
任何自訂 |
為了在 Spring Security 6.3 之前實現上述設定,我們必須自行發布此 bean,並確保我們也重新啟用了預設授權類型。若要了解幕後設定的內容,以下是設定可能的外觀
-
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 來啟用擴充授權類型的能力也提供了自訂現有授權類型的機會,而無需重新定義預設值。例如,如果我們想要自訂 ReactiveOAuth2AuthorizedClientProvider
中 client_credentials
授權的時鐘偏差,我們可以簡單地發布一個 bean,如下所示
-
Java
-
Kotlin
@Configuration
public class SecurityConfig {
@Bean
public ReactiveOAuth2AuthorizedClientProvider clientCredentials() {
ClientCredentialsReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
new ClientCredentialsReactiveOAuth2AuthorizedClientProvider();
authorizedClientProvider.setClockSkew(Duration.ofMinutes(5));
return authorizedClientProvider;
}
}
@Configuration
class SecurityConfig {
@Bean
fun clientCredentials(): ReactiveOAuth2AuthorizedClientProvider {
val authorizedClientProvider = ClientCredentialsReactiveOAuth2AuthorizedClientProvider()
authorizedClientProvider.setClockSkew(Duration.ofMinutes(5))
return authorizedClientProvider
}
}
自訂 Token 請求參數
在取得 access token 時,自訂請求參數的需求相當常見。例如,假設我們想要將自訂 audience
參數新增至 token 請求,因為提供者需要此參數用於 authorization_code
授權。
我們可以簡單地發布類型為 ReactiveOAuth2AccessTokenResponseClient
且泛型類型為 OAuth2AuthorizationCodeGrantRequest
的 bean,Spring Security 將使用它來設定 OAuth2 Client 元件。
以下範例自訂 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"
}
}
}
}
請注意,在這種情況下,我們不需要自訂 |
如您所見,將 ReactiveOAuth2AccessTokenResponseClient
作為 bean 提供非常方便。當直接使用 Spring Security DSL 時,我們需要確保此自訂同時應用於 OAuth2 登入 (如果我們正在使用此功能) 和 OAuth2 Client 元件。若要了解幕後設定的內容,以下是使用 DSL 設定的外觀
-
Java
-
Kotlin
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
WebClientReactiveAuthorizationCodeTokenResponseClient accessTokenResponseClient =
new WebClientReactiveAuthorizationCodeTokenResponseClient();
accessTokenResponseClient.addParametersConverter(parametersConverter());
http
.authorizeExchange((authorize) -> authorize
.anyExchange().authenticated()
)
.oauth2Login((oauth2Login) -> oauth2Login
.authenticationManager(new DelegatingReactiveAuthenticationManager(
new OidcAuthorizationCodeReactiveAuthenticationManager(
accessTokenResponseClient, new OidcReactiveOAuth2UserService()
),
new OAuth2LoginReactiveAuthenticationManager(
accessTokenResponseClient, new DefaultReactiveOAuth2UserService()
)
))
)
.oauth2Client((oauth2Client) -> oauth2Client
.authenticationManager(new OAuth2AuthorizationCodeReactiveAuthenticationManager(
accessTokenResponseClient
))
);
return http.build();
}
private static Converter<OAuth2AuthorizationCodeGrantRequest, MultiValueMap<String, String>> parametersConverter() {
// ...
}
}
import org.springframework.security.config.web.server.invoke
@Configuration
@EnableWebFluxSecurity
class SecurityConfig {
@Bean
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
val accessTokenResponseClient = WebClientReactiveAuthorizationCodeTokenResponseClient()
accessTokenResponseClient.addParametersConverter(parametersConverter())
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2Login {
authenticationManager = DelegatingReactiveAuthenticationManager(
OidcAuthorizationCodeReactiveAuthenticationManager(
accessTokenResponseClient, OidcReactiveOAuth2UserService()
),
OAuth2LoginReactiveAuthenticationManager(
accessTokenResponseClient, DefaultReactiveOAuth2UserService()
)
)
}
oauth2Client {
authenticationManager = OAuth2AuthorizationCodeReactiveAuthenticationManager(
accessTokenResponseClient
)
}
}
}
private fun parametersConverter(): Converter<OAuth2AuthorizationCodeGrantRequest, MultiValueMap<String, String>> {
// ...
}
}
對於其他授權類型,我們可以發布額外的 ReactiveOAuth2AccessTokenResponseClient
bean 來覆寫預設值。例如,若要自訂 client_credentials
授權的 token 請求,我們可以發布以下 bean
-
Java
-
Kotlin
@Configuration
public class SecurityConfig {
@Bean
public ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsAccessTokenResponseClient() {
WebClientReactiveClientCredentialsTokenResponseClient accessTokenResponseClient =
new WebClientReactiveClientCredentialsTokenResponseClient();
accessTokenResponseClient.addParametersConverter(parametersConverter());
return accessTokenResponseClient;
}
private static Converter<OAuth2ClientCredentialsGrantRequest, MultiValueMap<String, String>> parametersConverter() {
// ...
}
}
@Configuration
class SecurityConfig {
@Bean
fun clientCredentialsAccessTokenResponseClient(): ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> {
val accessTokenResponseClient = WebClientReactiveClientCredentialsTokenResponseClient()
accessTokenResponseClient.addParametersConverter(parametersConverter())
return accessTokenResponseClient
}
private fun parametersConverter(): Converter<OAuth2ClientCredentialsGrantRequest, MultiValueMap<String, String>> {
// ...
}
}
Spring Security 自動解析以下 ReactiveOAuth2AccessTokenResponseClient
bean 的泛型類型
-
OAuth2AuthorizationCodeGrantRequest
(請參閱WebClientReactiveAuthorizationCodeTokenResponseClient
) -
OAuth2RefreshTokenGrantRequest
(請參閱WebClientReactiveRefreshTokenTokenResponseClient
) -
OAuth2ClientCredentialsGrantRequest
(請參閱WebClientReactiveClientCredentialsTokenResponseClient
) -
OAuth2PasswordGrantRequest
(請參閱WebClientReactivePasswordTokenResponseClient
) -
JwtBearerGrantRequest
(請參閱WebClientReactiveJwtBearerTokenResponseClient
) -
TokenExchangeGrantRequest
(請參閱WebClientReactiveTokenExchangeTokenResponseClient
)
發布類型為 |
發布類型為 |
自訂 OAuth2 Client 元件使用的 WebClient
另一個常見的使用案例是需要自訂在取得 access token 時使用的 WebClient
。我們可能需要這樣做,以自訂底層 HTTP client 程式庫 (透過自訂 ClientHttpConnector
) 來設定 SSL 設定或為企業網路套用 proxy 設定。
在 Spring Security 6.3 及更新版本中,我們可以簡單地發布類型為 ReactiveOAuth2AccessTokenResponseClient
的 bean,Spring Security 將為我們設定並發布 ReactiveOAuth2AuthorizedClientManager
bean。
以下範例自訂所有支援的授權類型的 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。
請注意,在這種情況下,我們不需要自訂 |
在 Spring Security 6.3 之前,我們必須確保此自訂自行應用於 OAuth2 Client 元件。雖然我們可以為 authorization_code
授權發布類型為 ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest>
的 bean,但對於其他授權類型,我們必須發布類型為 ReactiveOAuth2AuthorizedClientManager
的 bean。若要了解幕後設定的內容,以下是設定可能的外觀
-
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 {
// ...
}
}