OAuth2 WebFlux
Spring Security 提供了全面的 OAuth 2.0 支持。本节讨论如何将 OAuth 2.0 集成到你的响应式应用中。
概览
Spring Security 的 OAuth 2.0 支持包含两个主要功能集
OAuth2 Login 是一个非常强大的 OAuth2 Client 功能,值得在参考文档中专门介绍。然而,它并非独立功能,需要 OAuth2 Client 才能正常工作。 |
这些功能集涵盖了 OAuth 2.0 授权框架中定义的 *资源服务器* 和 *客户端* 角色,而 *授权服务器* 角色由 Spring Authorization Server 覆盖,后者是基于 Spring Security 构建的一个独立项目。
OAuth2 中的 *资源服务器* 和 *客户端* 角色通常由一个或多个服务器端应用表示。此外,*授权服务器* 角色可以由一个或多个第三方表示(例如在组织内部集中身份管理和/或认证),**-或者-** 也可以由一个应用表示(例如 Spring Authorization Server)。
例如,典型的基于 OAuth2 的微服务架构可能包含一个面向用户的客户端应用、几个提供 REST API 的后端资源服务器以及一个用于管理用户和认证的第三方授权服务器。常见的情况是,单个应用仅代表其中一个角色,需要与提供其他角色的一或多个第三方集成。
Spring Security 能够处理这些以及更多的场景。以下章节涵盖了 Spring Security 提供的角色,并包含常见场景的示例。
OAuth2 Resource Server
本节包含 OAuth2 Resource Server 功能概述及示例。更多完整的参考文档请参阅 OAuth 2.0 Resource Server。 |
要开始使用,请将 spring-security-oauth2-resource-server
依赖添加到你的项目中。使用 Spring Boot 时,请添加以下启动器
-
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 保护 API 访问(授权服务器提供 JWT 或不透明访问令牌)
-
我想 使用 JWT 保护 API 访问(自定义令牌)
使用 OAuth2 访问令牌保护访问
使用 OAuth2 访问令牌保护 API 访问非常常见。在大多数情况下,Spring Security 只需最少的配置即可使用 OAuth2 保护应用。
Spring Security 支持两种类型的 Bearer
令牌,每种都使用不同的组件进行验证
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")
}
}
不透明令牌支持
以下示例使用 Spring Boot 配置属性配置一个 ReactiveOpaqueTokenIntrospector
bean
spring:
security:
oauth2:
resourceserver:
opaquetoken:
introspection-uri: https://my-auth-server.com/oauth2/introspect
client-id: my-client-id
client-secret: my-client-secret
使用 Spring Boot 时,只需这些配置。Spring Boot 提供的默认配置等同于以下内容
-
Java
-
Kotlin
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange((authorize) -> authorize
.anyExchange().authenticated()
)
.oauth2ResourceServer((oauth2) -> oauth2
.opaqueToken(Customizer.withDefaults())
);
return http.build();
}
@Bean
public ReactiveOpaqueTokenIntrospector opaqueTokenIntrospector() {
return new SpringReactiveOpaqueTokenIntrospector(
"https://my-auth-server.com/oauth2/introspect", "my-client-id", "my-client-secret");
}
}
import org.springframework.security.config.web.server.invoke
@Configuration
@EnableWebFluxSecurity
class SecurityConfig {
@Bean
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
opaqueToken { }
}
}
}
@Bean
fun opaqueTokenIntrospector(): ReactiveOpaqueTokenIntrospector {
return SpringReactiveOpaqueTokenIntrospector(
"https://my-auth-server.com/oauth2/introspect", "my-client-id", "my-client-secret"
)
}
}
使用自定义 JWT 保护访问
使用 JWT 保护 API 访问是一个相当常见的目标,特别是当前端作为单页应用开发时。Spring Security 中的 OAuth2 Resource Server 支持可用于任何类型的 Bearer
令牌,包括自定义 JWT。
使用 JWT 保护 API 所需的全部是 ReactiveJwtDecoder
bean,它用于验证签名和解码令牌。Spring Security 会自动使用提供的 bean 在 SecurityWebFilterChain
中配置保护。
以下示例使用 Spring Boot 配置属性配置一个 ReactiveJwtDecoder
bean
spring:
security:
oauth2:
resourceserver:
jwt:
public-key-location: classpath:my-public-key.pub
你可以将公钥作为 classpath 资源提供(本例中名为 |
使用 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 不提供用于铸造令牌的端点。但是,Spring Security 确实提供了 |
OAuth2 Client
本节包含 OAuth2 Client 功能概述及示例。更多完整的参考文档请参阅 OAuth 2.0 Client 和 OAuth 2.0 登录。 |
要开始使用,请将 spring-security-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 的使用场景
-
我想 为用户 获取访问令牌 以访问第三方 API
-
我想 两者都做(让用户登录 *并* 访问第三方 API)
-
我想 启用扩展授权模式
-
我想 定制现有授权模式
-
我想 定制令牌请求参数
使用 OAuth2 让用户登录
要求用户通过 OAuth2 登录非常常见。OpenID Connect 1.0 提供了一个名为 id_token
的特殊令牌,旨在赋予 OAuth2 Client 执行用户身份验证和让用户登录的能力。在某些情况下,OAuth2 可以直接用于让用户登录(例如,GitHub 和 Facebook 等未实现 OpenID Connect 的流行社交登录提供商)。
以下示例将应用配置为 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 { }
}
}
}
除了上述配置,应用还需要通过使用 ReactiveClientRegistrationRepository
bean 来配置至少一个 ClientRegistration
。以下示例使用 Spring Boot 配置属性配置一个 InMemoryReactiveClientRegistrationRepository
bean
spring:
security:
oauth2:
client:
registration:
my-oidc-client:
provider: my-oidc-provider
client-id: my-client-id
client-secret: my-client-secret
authorization-grant-type: authorization_code
scope: openid,profile
provider:
my-oidc-provider:
issuer-uri: https://my-oidc-provider.com
有了上述配置,应用现在支持两个额外的端点
-
登录端点(例如
/oauth2/authorization/my-oidc-client
)用于启动登录并重定向到第三方授权服务器。 -
重定向端点(例如
/login/oauth2/code/my-oidc-client
)由授权服务器用于重定向回客户端应用,并将包含一个code
参数,通过访问令牌请求用于获取id_token
和/或access_token
。
上述配置中 |
访问受保护资源
对受 OAuth2 保护的第三方 API 发出请求是 OAuth2 Client 的核心用例。这通过授权客户端(在 Spring Security 中由 OAuth2AuthorizedClient
类表示)并通过在外发请求的 Authorization
头部放置 Bearer
令牌来访问受保护资源来实现。
以下示例将应用配置为 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 { }
}
}
}
上述示例未提供用户登录的方式。你可以使用任何其他登录机制(例如 |
除了上述配置,应用还需要通过使用 ReactiveClientRegistrationRepository
bean 来配置至少一个 ClientRegistration
。以下示例使用 Spring Boot 配置属性配置一个 InMemoryReactiveClientRegistrationRepository
bean
spring:
security:
oauth2:
client:
registration:
my-oauth2-client:
provider: my-auth-server
client-id: my-client-id
client-secret: my-client-secret
authorization-grant-type: authorization_code
scope: message.read,message.write
provider:
my-auth-server:
issuer-uri: https://my-auth-server.com
除了配置 Spring Security 以支持 OAuth2 Client 功能外,你还需要决定如何访问受保护资源并相应地配置你的应用。Spring Security 提供了 ReactiveOAuth2AuthorizedClientManager
的实现,用于获取可用于访问受保护资源的访问令牌。
当不存在 |
使用 ReactiveOAuth2AuthorizedClientManager
的最简单方法是通过一个 ExchangeFilterFunction
,它通过 WebClient
拦截请求。
以下示例使用默认的 ReactiveOAuth2AuthorizedClientManager
配置一个 WebClient
,该 WebClient
能够通过在每个请求的 Authorization
头部放置 Bearer
令牌来访问受保护资源
ExchangeFilterFunction
配置 WebClient
-
Java
-
Kotlin
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
ServerOAuth2AuthorizedClientExchangeFilterFunction filter =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
return WebClient.builder()
.filter(filter)
.build();
}
}
@Configuration
class WebClientConfig {
@Bean
fun webClient(authorizedClientManager: ReactiveOAuth2AuthorizedClientManager): WebClient {
val filter = ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager)
return WebClient.builder()
.filter(filter)
.build()
}
}
这个配置好的 WebClient
可以按以下示例使用
WebClient
访问受保护资源-
Java
-
Kotlin
import static org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId;
@RestController
public class MessagesController {
private final WebClient webClient;
public MessagesController(WebClient webClient) {
this.webClient = webClient;
}
@GetMapping("/messages")
public Mono<ResponseEntity<List<Message>>> messages() {
return this.webClient.get()
.uri("http://localhost: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("http://localhost:8090/messages")
.attributes(clientRegistrationId("my-oauth2-client"))
.retrieve()
.toEntityList<Message>()
}
data class Message(val message: String)
}
访问当前用户的受保护资源
当用户通过 OAuth2 或 OpenID Connect 登录时,授权服务器可能会提供一个可直接用于访问受保护资源的访问令牌。这很方便,因为它只需配置一个 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 { }
}
}
}
除了上述配置,应用还需要通过使用 ReactiveClientRegistrationRepository
bean 来配置至少一个 ClientRegistration
。以下示例使用 Spring Boot 配置属性配置一个 InMemoryReactiveClientRegistrationRepository
bean
spring:
security:
oauth2:
client:
registration:
my-combined-client:
provider: my-auth-server
client-id: my-client-id
client-secret: my-client-secret
authorization-grant-type: authorization_code
scope: openid,profile,message.read,message.write
provider:
my-auth-server:
issuer-uri: https://my-auth-server.com
前面示例(使用 OAuth2 让用户登录、访问受保护资源)与本例的主要区别在于通过 |
除了配置 Spring Security 以支持 OAuth2 Client 功能外,你还需要决定如何访问受保护资源并相应地配置你的应用。Spring Security 提供了 ReactiveOAuth2AuthorizedClientManager
的实现,用于获取可用于访问受保护资源的访问令牌。
当不存在 |
使用 ReactiveOAuth2AuthorizedClientManager
的最简单方法是通过一个 ExchangeFilterFunction
,它通过 WebClient
拦截请求。
以下示例使用默认的 ReactiveOAuth2AuthorizedClientManager
配置一个 WebClient
,该 WebClient
能够通过在每个请求的 Authorization
头部放置 Bearer
令牌来访问受保护资源
ExchangeFilterFunction
配置 WebClient
-
Java
-
Kotlin
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
ServerOAuth2AuthorizedClientExchangeFilterFunction filter =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
return WebClient.builder()
.filter(filter)
.build();
}
}
@Configuration
class WebClientConfig {
@Bean
fun webClient(authorizedClientManager: ReactiveOAuth2AuthorizedClientManager): WebClient {
val filter = ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager)
return WebClient.builder()
.filter(filter)
.build()
}
}
这个配置好的 WebClient
可以按以下示例使用
WebClient
访问受保护资源(当前用户)-
Java
-
Kotlin
@RestController
public class MessagesController {
private final WebClient webClient;
public MessagesController(WebClient webClient) {
this.webClient = webClient;
}
@GetMapping("/messages")
public Mono<ResponseEntity<List<Message>>> messages() {
return this.webClient.get()
.uri("http://localhost: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("http://localhost: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
授权模式
jwt-bearer
授权模式-
Java
-
Kotlin
@Configuration
public class SecurityConfig {
@Bean
public ReactiveOAuth2AuthorizedClientProvider jwtBearer() {
return new JwtBearerReactiveOAuth2AuthorizedClientProvider();
}
}
@Configuration
class SecurityConfig {
@Bean
fun jwtBearer(): ReactiveOAuth2AuthorizedClientProvider {
return JwtBearerReactiveOAuth2AuthorizedClientProvider()
}
}
当没有提供 ReactiveOAuth2AuthorizedClientManager
bean 时,Spring Security 会自动发布一个默认的。
任何自定义的 |
为了在 Spring Security 6.3 之前实现上述配置,我们必须自己发布这个 bean,并确保我们也重新启用了默认的授权模式。为了理解其背后配置的原理,以下是配置示例
jwt-bearer
授权模式(6.3 之前)-
Java
-
Kotlin
@Configuration
public class SecurityConfig {
@Bean
public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.clientCredentials()
.password()
.provider(new JwtBearerReactiveOAuth2AuthorizedClientProvider())
.build();
DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
}
@Configuration
class SecurityConfig {
@Bean
fun authorizedClientManager(
clientRegistrationRepository: ReactiveClientRegistrationRepository,
authorizedClientRepository: ServerOAuth2AuthorizedClientRepository
): ReactiveOAuth2AuthorizedClientManager {
val authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.clientCredentials()
.password()
.provider(JwtBearerReactiveOAuth2AuthorizedClientProvider())
.build()
val authorizedClientManager = DefaultReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository
)
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
return authorizedClientManager
}
}
定制现有授权模式
通过发布 bean 启用扩展授权模式的能力也为定制现有授权模式提供了机会,而无需重新定义默认值。例如,如果我们想定制 client_credentials
授权模式的 ReactiveOAuth2AuthorizedClientProvider
的时钟偏移,我们只需像这样发布一个 bean
-
Java
-
Kotlin
@Configuration
public class SecurityConfig {
@Bean
public ReactiveOAuth2AuthorizedClientProvider clientCredentials() {
ClientCredentialsReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
new ClientCredentialsReactiveOAuth2AuthorizedClientProvider();
authorizedClientProvider.setClockSkew(Duration.ofMinutes(5));
return authorizedClientProvider;
}
}
@Configuration
class SecurityConfig {
@Bean
fun clientCredentials(): ReactiveOAuth2AuthorizedClientProvider {
val authorizedClientProvider = ClientCredentialsReactiveOAuth2AuthorizedClientProvider()
authorizedClientProvider.setClockSkew(Duration.ofMinutes(5))
return authorizedClientProvider
}
}
定制令牌请求参数
在获取访问令牌时定制请求参数的需求相当常见。例如,假设我们想向令牌请求中添加一个自定义的 audience
参数,因为提供者对 authorization_code
授权模式要求此参数。
我们只需发布一个泛型类型为 OAuth2AuthorizationCodeGrantRequest
的 ReactiveOAuth2AccessTokenResponseClient
类型 bean,Spring Security 将使用它来配置 OAuth2 Client 组件。
以下示例定制了 authorization_code
授权模式的令牌请求参数
-
Java
-
Kotlin
@Configuration
public class SecurityConfig {
@Bean
public ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> authorizationCodeAccessTokenResponseClient() {
WebClientReactiveAuthorizationCodeTokenResponseClient accessTokenResponseClient =
new WebClientReactiveAuthorizationCodeTokenResponseClient();
accessTokenResponseClient.addParametersConverter(parametersConverter());
return accessTokenResponseClient;
}
private static Converter<OAuth2AuthorizationCodeGrantRequest, MultiValueMap<String, String>> parametersConverter() {
return (grantRequest) -> {
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set("audience", "xyz_value");
return parameters;
};
}
}
@Configuration
class SecurityConfig {
@Bean
fun authorizationCodeAccessTokenResponseClient(): ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
val accessTokenResponseClient = WebClientReactiveAuthorizationCodeTokenResponseClient()
accessTokenResponseClient.addParametersConverter(parametersConverter())
return accessTokenResponseClient
}
private fun parametersConverter(): Converter<OAuth2AuthorizationCodeGrantRequest, MultiValueMap<String, String>> {
return Converter<OAuth2AuthorizationCodeGrantRequest, MultiValueMap<String, String>> { grantRequest ->
LinkedMultiValueMap<String, String>().also { parameters ->
parameters["audience"] = "xyz_value"
}
}
}
}
请注意,在这种情况下我们无需定制 |
如你所见,将 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
授权模式的令牌请求,我们可以发布以下 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
另一个常见用例是需要定制获取访问令牌时使用的 WebClient
。我们可能需要通过定制底层 HTTP 客户端库(通过自定义 ClientHttpConnector
)来配置 SSL 设置或为企业网络应用代理设置。
在 Spring Security 6.3 及更高版本中,我们只需发布 ReactiveOAuth2AccessTokenResponseClient
类型的 bean,Spring Security 将为我们配置并发布一个 ReactiveOAuth2AuthorizedClientManager
bean。
以下示例为所有支持的授权模式定制了 WebClient
WebClient
-
Java
-
Kotlin
@Configuration
public class SecurityConfig {
@Bean
public ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> authorizationCodeAccessTokenResponseClient() {
WebClientReactiveAuthorizationCodeTokenResponseClient accessTokenResponseClient =
new WebClientReactiveAuthorizationCodeTokenResponseClient();
accessTokenResponseClient.setWebClient(webClient());
return accessTokenResponseClient;
}
@Bean
public ReactiveOAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenAccessTokenResponseClient() {
WebClientReactiveRefreshTokenTokenResponseClient accessTokenResponseClient =
new WebClientReactiveRefreshTokenTokenResponseClient();
accessTokenResponseClient.setWebClient(webClient());
return accessTokenResponseClient;
}
@Bean
public ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsAccessTokenResponseClient() {
WebClientReactiveClientCredentialsTokenResponseClient accessTokenResponseClient =
new WebClientReactiveClientCredentialsTokenResponseClient();
accessTokenResponseClient.setWebClient(webClient());
return accessTokenResponseClient;
}
@Bean
public ReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> passwordAccessTokenResponseClient() {
WebClientReactivePasswordTokenResponseClient accessTokenResponseClient =
new WebClientReactivePasswordTokenResponseClient();
accessTokenResponseClient.setWebClient(webClient());
return accessTokenResponseClient;
}
@Bean
public ReactiveOAuth2AccessTokenResponseClient<JwtBearerGrantRequest> jwtBearerAccessTokenResponseClient() {
WebClientReactiveJwtBearerTokenResponseClient accessTokenResponseClient =
new WebClientReactiveJwtBearerTokenResponseClient();
accessTokenResponseClient.setWebClient(webClient());
return accessTokenResponseClient;
}
@Bean
public ReactiveOAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> tokenExchangeAccessTokenResponseClient() {
WebClientReactiveTokenExchangeTokenResponseClient accessTokenResponseClient =
new WebClientReactiveTokenExchangeTokenResponseClient();
accessTokenResponseClient.setWebClient(webClient());
return accessTokenResponseClient;
}
@Bean
public WebClient webClient() {
// ...
}
}
@Configuration
class SecurityConfig {
@Bean
fun authorizationCodeAccessTokenResponseClient(): ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
val accessTokenResponseClient = WebClientReactiveAuthorizationCodeTokenResponseClient()
accessTokenResponseClient.setWebClient(webClient())
return accessTokenResponseClient
}
@Bean
fun refreshTokenAccessTokenResponseClient(): ReactiveOAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> {
val accessTokenResponseClient = WebClientReactiveRefreshTokenTokenResponseClient()
accessTokenResponseClient.setWebClient(webClient())
return accessTokenResponseClient
}
@Bean
fun clientCredentialsAccessTokenResponseClient(): ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> {
val accessTokenResponseClient = WebClientReactiveClientCredentialsTokenResponseClient()
accessTokenResponseClient.setWebClient(webClient())
return accessTokenResponseClient
}
@Bean
fun passwordAccessTokenResponseClient(): ReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> {
val accessTokenResponseClient = WebClientReactivePasswordTokenResponseClient()
accessTokenResponseClient.setWebClient(webClient())
return accessTokenResponseClient
}
@Bean
fun jwtBearerAccessTokenResponseClient(): ReactiveOAuth2AccessTokenResponseClient<JwtBearerGrantRequest> {
val accessTokenResponseClient = WebClientReactiveJwtBearerTokenResponseClient()
accessTokenResponseClient.setWebClient(webClient())
return accessTokenResponseClient
}
@Bean
fun tokenExchangeAccessTokenResponseClient(): ReactiveOAuth2AccessTokenResponseClient<TokenExchangeGrantRequest> {
val accessTokenResponseClient = WebClientReactiveTokenExchangeTokenResponseClient()
accessTokenResponseClient.setWebClient(webClient())
return accessTokenResponseClient
}
@Bean
fun webClient(): WebClient {
// ...
}
}
当没有提供 ReactiveOAuth2AuthorizedClientManager
bean 时,Spring Security 会自动发布一个默认的。
请注意,在这种情况下我们无需定制 |
在 Spring Security 6.3 之前,我们必须自己确保将此定制应用于 OAuth2 Client 组件。虽然我们可以为 authorization_code
授权模式发布一个 ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest>
类型的 bean,但对于其他授权模式,我们必须发布一个 ReactiveOAuth2AuthorizedClientManager
类型的 bean。为了理解其背后配置的原理,以下是配置示例
WebClient
(6.3 之前)-
Java
-
Kotlin
@Configuration
public class SecurityConfig {
@Bean
public ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> authorizationCodeAccessTokenResponseClient() {
WebClientReactiveAuthorizationCodeTokenResponseClient accessTokenResponseClient =
new WebClientReactiveAuthorizationCodeTokenResponseClient();
accessTokenResponseClient.setWebClient(webClient());
return accessTokenResponseClient;
}
@Bean
public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
WebClientReactiveRefreshTokenTokenResponseClient refreshTokenAccessTokenResponseClient =
new WebClientReactiveRefreshTokenTokenResponseClient();
refreshTokenAccessTokenResponseClient.setWebClient(webClient());
WebClientReactiveClientCredentialsTokenResponseClient clientCredentialsAccessTokenResponseClient =
new WebClientReactiveClientCredentialsTokenResponseClient();
clientCredentialsAccessTokenResponseClient.setWebClient(webClient());
WebClientReactivePasswordTokenResponseClient passwordAccessTokenResponseClient =
new WebClientReactivePasswordTokenResponseClient();
passwordAccessTokenResponseClient.setWebClient(webClient());
WebClientReactiveJwtBearerTokenResponseClient jwtBearerAccessTokenResponseClient =
new WebClientReactiveJwtBearerTokenResponseClient();
jwtBearerAccessTokenResponseClient.setWebClient(webClient());
JwtBearerReactiveOAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider =
new JwtBearerReactiveOAuth2AuthorizedClientProvider();
jwtBearerAuthorizedClientProvider.setAccessTokenResponseClient(jwtBearerAccessTokenResponseClient);
WebClientReactiveTokenExchangeTokenResponseClient tokenExchangeAccessTokenResponseClient =
new WebClientReactiveTokenExchangeTokenResponseClient();
tokenExchangeAccessTokenResponseClient.setWebClient(webClient());
TokenExchangeReactiveOAuth2AuthorizedClientProvider tokenExchangeAuthorizedClientProvider =
new TokenExchangeReactiveOAuth2AuthorizedClientProvider();
tokenExchangeAuthorizedClientProvider.setAccessTokenResponseClient(tokenExchangeAccessTokenResponseClient);
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken((refreshToken) -> refreshToken
.accessTokenResponseClient(refreshTokenAccessTokenResponseClient)
)
.clientCredentials((clientCredentials) -> clientCredentials
.accessTokenResponseClient(clientCredentialsAccessTokenResponseClient)
)
.password((password) -> password
.accessTokenResponseClient(passwordAccessTokenResponseClient)
)
.provider(jwtBearerAuthorizedClientProvider)
.provider(tokenExchangeAuthorizedClientProvider)
.build();
DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
@Bean
public WebClient webClient() {
// ...
}
}
import org.springframework.security.config.web.server.invoke
@Configuration
class SecurityConfig {
@Bean
fun authorizationCodeAccessTokenResponseClient(): ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
val accessTokenResponseClient = WebClientReactiveAuthorizationCodeTokenResponseClient()
accessTokenResponseClient.setWebClient(webClient())
return accessTokenResponseClient
}
@Bean
fun authorizedClientManager(
clientRegistrationRepository: ReactiveClientRegistrationRepository?,
authorizedClientRepository: ServerOAuth2AuthorizedClientRepository?
): ReactiveOAuth2AuthorizedClientManager {
val refreshTokenAccessTokenResponseClient = WebClientReactiveRefreshTokenTokenResponseClient()
refreshTokenAccessTokenResponseClient.setWebClient(webClient())
val clientCredentialsAccessTokenResponseClient = WebClientReactiveClientCredentialsTokenResponseClient()
clientCredentialsAccessTokenResponseClient.setWebClient(webClient())
val passwordAccessTokenResponseClient = WebClientReactivePasswordTokenResponseClient()
passwordAccessTokenResponseClient.setWebClient(webClient())
val jwtBearerAccessTokenResponseClient = WebClientReactiveJwtBearerTokenResponseClient()
jwtBearerAccessTokenResponseClient.setWebClient(webClient())
val jwtBearerAuthorizedClientProvider = JwtBearerReactiveOAuth2AuthorizedClientProvider()
jwtBearerAuthorizedClientProvider.setAccessTokenResponseClient(jwtBearerAccessTokenResponseClient)
val tokenExchangeAccessTokenResponseClient = WebClientReactiveTokenExchangeTokenResponseClient()
tokenExchangeAccessTokenResponseClient.setWebClient(webClient())
val tokenExchangeAuthorizedClientProvider = TokenExchangeReactiveOAuth2AuthorizedClientProvider()
tokenExchangeAuthorizedClientProvider.setAccessTokenResponseClient(tokenExchangeAccessTokenResponseClient)
val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken { refreshToken ->
refreshToken.accessTokenResponseClient(refreshTokenAccessTokenResponseClient)
}
.clientCredentials { clientCredentials ->
clientCredentials.accessTokenResponseClient(clientCredentialsAccessTokenResponseClient)
}
.password { password ->
password.accessTokenResponseClient(passwordAccessTokenResponseClient)
}
.provider(jwtBearerAuthorizedClientProvider)
.provider(tokenExchangeAuthorizedClientProvider)
.build()
val authorizedClientManager = DefaultReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository
)
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
return authorizedClientManager
}
@Bean
fun webClient(): WebClient {
// ...
}
}