OAuth2

Spring Security 提供全面的 OAuth 2.0 支持。此部分讨论如何将 OAuth 2.0 集成到基于 Servlet 的应用程序中。

Overview

Spring Security 的 OAuth 2.0 支持包括两组主要功能:

OAuth2 Login 是一个非常强大的 OAuth2 客户端功能,值得在参考文档中专门进行论述。然而,它并不作为一个独立的功能而存在,需要 OAuth2 客户端才能发挥作用。

这些功能集涵盖了 OAuth 2.0 Authorization Framework中定义的 _resource server_和 _client_角色,而 _authorization server_角色由 Spring Authorization Server涵盖,它是在 Spring Security之上构建的独立项目。

OAuth2 中的 resource serverclient 角色通常由一个或多个服务器端应用程序来表示。此外,authorization server 角色可以由一个或多个第三方来表示(例如在组织内集中身份管理和/或身份验证时),-or- 它可以由应用程序来表示(例如 Spring 授权服务器)。

例如,一个典型的基于 OAuth2 的微服务架构可能由一个用户界面客户端应用程序、几个为 REST API 提供服务的后台资源服务器和一个为用户和身份验证问题进行管理的第三方授权服务器组成。同样常见的情况是,单个应用程序仅表示这些角色中的一个,需要与提供其他角色的一个或多个第三方进行集成。

Spring Security 可处理这些场景以及更多场景。以下小节涵盖了 Spring Security 提供的角色,并包含了一些常见场景的示例。

OAuth2 Resource Server

本部分包含有关 OAuth2 资源服务器功能的摘要和示例。有关完整参考文档,请参阅 OAuth 2.0 Resource Server

要开始使用,请将 spring-security-oauth2-resource-server 依赖项添加到项目中。在使用 Spring Boot 时,添加以下 starter:

OAuth2 Client with 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 时,有关其他选项,请参阅 Getting Spring Security

考虑以下 OAuth2 资源服务器用例:

Protect Access with an OAuth2 Access Token

使用 OAuth2 访问令牌来保护对 API 的访问非常常见。在大多数情况下,Spring Security 只需对应用程序进行最小配置即可使用 OAuth2 进行保护。

Spring Security 支持两种类型的 Bearer 令牌,每种令牌使用不同的组件进行验证:

JWT Support

以下示例是使用 Spring Boot 配置属性来配置 JwtDecoder bean 的情况:

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

在使用 Spring Boot 时,这便是全部所需。Spring Boot 提供的默认配置等同于以下内容:

Configure Resource Server with JWTs
  • 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")
	}

}

Opaque Token Support

以下示例是使用 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 提供的默认配置等同于以下内容:

Configure Resource Server with Opaque Tokens
  • 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"
		)
	}

}

Protect Access with a custom 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 提供的默认配置等同于以下内容:

Configure Resource Server with Custom JWTs
  • Java

  • Kotlin

@Configuration
@EnableWebSecurity
public class SecurityConfig {

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

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

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

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

@Configuration
@EnableWebSecurity
class SecurityConfig {

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

		return http.build()
	}

	@Bean
	fun jwtDecoder(): JwtDecoder {
		return NimbusJwtDecoder.withPublicKey(publicKey()).build()
	}

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

}

Spring Security 不提供用于铸造令牌的端点。但是,Spring Security 确实提供了 JwtEncoder 接口以及一个实现,即 NimbusJwtEncoder

OAuth2 Client

本部分包含有关 OAuth2 客户端功能的摘要和示例。有关完整参考文档,请参阅 OAuth 2.0 ClientOAuth 2.0 Login

要开始使用,请将 spring-security-oauth2-client 依赖项添加到项目中。在使用 Spring Boot 时,添加以下 starter:

OAuth2 Client with 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 时,有关其他选项,请参阅 Getting Spring Security

考虑 OAuth2 客户端的以下用例:

Log Users In with OAuth2

通常要求用户通过 OAuth2 登录。 OpenID Connect 1.0 提供一个名为 id_token 的专用令牌,该令牌旨在向 OAuth2 客户端提供执行用户身份验证和登录用户的能力。在某些情况下,OAuth2 可直接用于登录用户(如不实现 OpenID Connect 的流行社交登录提供商,例如 GitHub 和 Facebook)。

以下示例将应用程序配置为充当一个能够使用 OAuth2 或 OpenID Connect 登录用户的 OAuth2 客户端:

Configure OAuth2 Login
  • 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 特定组件(如 OAuth2UserService)。

Access Protected Resources

对受 OAuth2 保护的第三方 API 发出请求是 OAuth2 客户端的核心用例。这是通过授权客户端(由 Spring Security 中的 OAuth2AuthorizedClient 类表示)和通过将 Bearer 令牌放入传出请求的 Authorization 头中来访问受保护的资源来实现的。

以下示例将应用程序配置为充当一个能够从第三方 API 请求受保护资源的 OAuth2 客户端:

Configure OAuth2 Client
  • Java

  • Kotlin

@Configuration
@EnableWebSecurity
public class SecurityConfig {

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

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

@Configuration
@EnableWebSecurity
class SecurityConfig {

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

		return http.build()
	}

}

以上示例并未提供用户登录的方式。你可以使用任何其他登录机制(如 formLogin())。请参阅 next section,了解将 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 Bean。

使用 OAuth2AuthorizedClientManager 的最简单方法是通过 ExchangeFilterFunction 拦截 WebClient 中的请求。要使用 WebClient,你需要添加 spring-webflux 依赖项以及一个反应式客户端实现:

Add Spring WebFlux Dependency
  • Gradle

  • Maven

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

以下示例使用默认 OAuth2AuthorizedClientManager 来配置一个 WebClient,该 WebClient 能够通过将 Bearer 令牌放入每个请求的 Authorization 头中来访问受保护的资源:

Configure WebClient with ExchangeFilterFunction
  • 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 可用于以下示例中:

Use WebClient to Access Protected Resources
  • 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("http://localhost: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("http://localhost:8090/messages")
			.attributes(clientRegistrationId("my-oauth2-client"))
			.retrieve()
			.toEntityList(Message::class.java)
			.block()!!
	}

	data class Message(val message: String)

}

Access Protected Resources for the Current User

当用户通过 OAuth2 或 OpenID Connect 登录时,授权服务器可能会提供一个直接用于访问受保护资源的访问令牌。这很方便,因为它只需要为两个用例同时配置一个 ClientRegistration

此部分将 Log Users In with OAuth2Access Protected Resources 合并到一个配置中。还存在其他高级场景,例如为登录配置一个 ClientRegistration,而为访问受保护资源配置另一个。所有此类场景都将使用相同的基本配置。

以下示例将应用程序配置为充当一个能够登录 and 用户、请求来自第三方 API 的受保护资源的 OAuth2 客户端:

Configure OAuth2 Login and OAuth2 Client
  • Java

  • Kotlin

@Configuration
@EnableWebSecurity
public class SecurityConfig {

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

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

@Configuration
@EnableWebSecurity
class SecurityConfig {

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

		return http.build()
	}

}

除了以上配置,应用程序还需要至少一个 ClientRegistration,通过使用 ClientRegistrationRepository Bean 进行配置。以下示例使用 Spring Boot 配置属性来配置一个 InMemoryClientRegistrationRepository Bean:

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

以前示例(Log Users In with OAuth2Access Protected Resources)与此示例之间的主要区别在于通过 scope 属性配置的内容,它将标准范围 openidprofile 与自定义范围 message.readmessage.write 相结合。

除了将 Spring Security 配置为支持 OAuth2 客户端功能之外,你还需要决定如何访问受保护的资源,并相应地配置应用程序。Spring Security 提供了 OAuth2AuthorizedClientManager 的实现,用于获取可用于访问受保护资源的访问令牌。

当不存在 OAuth2AuthorizedClientManager Bean 时,Spring Security 会为你注册一个默认 OAuth2AuthorizedClientManager Bean。

使用 OAuth2AuthorizedClientManager 的最简单方法是通过 ExchangeFilterFunction 拦截 WebClient 中的请求。要使用 WebClient,你需要添加 spring-webflux 依赖项以及一个反应式客户端实现:

Add Spring WebFlux Dependency
  • Gradle

  • Maven

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

以下示例使用默认 OAuth2AuthorizedClientManager 来配置一个 WebClient,该 WebClient 能够通过将 Bearer 令牌放入每个请求的 Authorization 头中来访问受保护的资源:

Configure WebClient with ExchangeFilterFunction
  • 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 可用于以下示例中:

Use WebClient to Access Protected Resources (Current User)
  • 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("http://localhost: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("http://localhost:8090/messages")
			.retrieve()
			.toEntityList(Message::class.java)
			.block()!!
	}

	data class Message(val message: String)

}

previous example 不同,请注意,我们不需要告诉 Spring Security 我们希望使用的 clientRegistrationId。这是因为它可以从当前登录用户那里获取。

Enable an Extension Grant Type

一个常见的用例涉及启用和/或配置扩展授权类型。例如,Spring Security 提供了对 jwt-bearer 授权类型的支持,但默认情况下未启用它,因为它不属于核心 OAuth 2.0 规范的一部分。

借助 Spring Security 6.2 及更高版本,我们可以简单地为一个或多个 OAuth2AuthorizedClientProvider 发布一个 bean,而系统将自动选择这些 bean。以下示例只需启用 jwt-bearer 授予类型:

Enable jwt-bearer Grant Type
  • Java

  • Kotlin

@Configuration
public class SecurityConfig {

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

}
@Configuration
class SecurityConfig {

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

}

当Spring Security尚未提供 OAuth2AuthorizedClientManager 时,它将自动发布默认 OAuth2AuthorizedClientManager

自定义 OAuth2AuthorizedClientProvider bean 也会在默认授权类型后被选取并应用到提供的 OAuth2AuthorizedClientManager

为了在 Spring Security 6.2 之前实现上述配置,我们必须自行发布该 bean,确保重新启用默认授权类型。为了了解幕后正在配置什么,以下是配置的可能样子:

Enable jwt-bearer Grant Type (prior to 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
	}

}

Customize an Existing Grant Type

通过发布 bean 进行 enable extension grant types 的功能还提供了定制现有授予类型的机会,而无需重新定义默认设置。例如,如果我们想针对 client_credentials 授予自定义 OAuth2AuthorizedClientProvider 的时钟偏移,我们可以简单地发布如下 bean:

Customize Client Credentials Grant Type
  • 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
	}

}

Customize Token Request Parameters

在获取访问令牌时,需要定制请求参数的情况非常普遍。例如,假设我们要向令牌请求中添加自定义 audience 参数,因为提供商需要此参数才能进行 authorization_code 授予。

借助 Spring Security 6.2 及更高版本,我们只需发布类型为 OAuth2AccessTokenResponseClient 且泛型类型为 OAuth2AuthorizationCodeGrantRequest 的 bean,Spring Security 就会用它来配置 OAuth2 客户端组件。

以下示例未使用 DSL 定制了 authorization_code 授予的令牌请求参数:

Customize Token Request Parameters for Authorization Code Grant
  • 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 登录(如果我们正在使用此功能)和 OAuth2 客户端组件,并使用 Spring Security DSL。为了解幕后正在配置什么,以下是配置的可能样子:

Customize Token Request Parameters for Authorization Code Grant (prior to 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:

Customize Token Request Parameters for Client Credentials Grant
  • 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 (see DefaultAuthorizationCodeTokenResponseClient)

  • OAuth2RefreshTokenGrantRequest (see DefaultRefreshTokenTokenResponseClient)

  • OAuth2ClientCredentialsGrantRequest (see DefaultClientCredentialsTokenResponseClient)

  • OAuth2PasswordGrantRequest (see DefaultPasswordTokenResponseClient)

  • JwtBearerGrantRequest (see DefaultJwtBearerTokenResponseClient)

发布类型为 OAuth2AccessTokenResponseClient<JwtBearerGrantRequest> 的 bean 会自动启用 jwt-bearer 授权类型,而无需 configure it separately

Customize the RestOperations used by OAuth2 Client Components

另一种常见用例是在获取访问令牌时需要自定义 RestOperations。我们可能需要执行此操作以定制对响应的处理(通过自定义 HttpMessageConverter)或为公司网络应用代理设置(通过定制 ClientHttpRequestFactory)。

借助 Spring Security 6.2 及更高版本,我们只需发布类型为 OAuth2AccessTokenResponseClient 的 bean,Spring Security 就会为我们配置并发布 OAuth2AuthorizedClientManager bean。

以下示例为所有受支持的授权类型定制 RestOperations

Customize RestOperations for OAuth2 Client
  • 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 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 restTemplate(): RestTemplate {
		// ...
	}

}

当Spring Security尚未提供 OAuth2AuthorizedClientManager 时,它将自动发布默认 OAuth2AuthorizedClientManager

请注意,在这种情况下,我们无需定制 SecurityFilterChain bean,并且可坚持使用默认设置。如果在没有任何其他定制的情况下使用 Spring Boot,我们实际上可以完全忽略 SecurityFilterChain bean。

在 Spring Security 6.2 之前,我们必须确保此定制适用于 OAuth2 登录(如果我们正在使用此功能)和 OAuth2 客户端组件。我们必须同时使用 Spring Security DSL(对于 authorization_code 授予)和为其他授予类型发布类型为 OAuth2AuthorizedClientManager 的 bean。为了了解幕后正在配置什么,以下是配置的可能样子:

Customize RestOperations for OAuth2 Client (prior to 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);

		OAuth2AuthorizedClientProvider authorizedClientProvider =
			OAuth2AuthorizedClientProviderBuilder.builder()
				.authorizationCode()
				.refreshToken((refreshToken) -> refreshToken
					.accessTokenResponseClient(refreshTokenAccessTokenResponseClient)
				)
				.clientCredentials((clientCredentials) -> clientCredentials
					.accessTokenResponseClient(clientCredentialsAccessTokenResponseClient)
				)
				.password((password) -> password
					.accessTokenResponseClient(passwordAccessTokenResponseClient)
				)
				.provider(jwtBearerAuthorizedClientProvider)
				.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 authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
			.authorizationCode()
			.refreshToken { refreshToken ->
				refreshToken.accessTokenResponseClient(refreshTokenAccessTokenResponseClient)
			}
			.clientCredentials { clientCredentials ->
				clientCredentials.accessTokenResponseClient(clientCredentialsAccessTokenResponseClient)
			}
			.password { password ->
				password.accessTokenResponseClient(passwordAccessTokenResponseClient)
			}
			.provider(jwtBearerAuthorizedClientProvider)
			.build()

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

		return authorizedClientManager
	}

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

}

Further Reading

前面部分介绍了 Spring Security 对 OAuth2 的支持以及针对常见方案的示例。您可以在参考文档的以下部分阅读有关 OAuth2 客户端和资源服务器的更多信息: