此版本仍在开发中,尚未被视为稳定版本。如需最新稳定版本,请使用 Spring Security 7.0.4spring-doc.cadn.net.cn

OAuth2

Spring Security 提供了全面的 OAuth 2.0 支持。 本节讨论如何将 OAuth 2.0 集成到基于 Servlet 的应用程序中。spring-doc.cadn.net.cn

概述

Spring Security 的 OAuth 2.0 支持包括两个主要的功能集:spring-doc.cadn.net.cn

OAuth2 登录 是一个非常强大的 OAuth2 客户端功能,在参考文档中值得拥有自己独立的一节。 然而,它并非一个独立的功能,需要依赖 OAuth2 客户端才能正常运行。spring-doc.cadn.net.cn

这些功能集涵盖了在OAuth 2.0授权框架中定义的资源服务器客户端角色,而授权服务器角色则由独立项目Spring Authorization Server覆盖,该项目基于Spring Security构建。spring-doc.cadn.net.cn

The 资源服务器客户端在OAuth2中的角色通常由一个或多个后端应用程序表示。 此外,授权服务器的角色可以由一个或多个第三方代表(例如,在组织内部集中身份管理和/或认证的情况下)-或者-它可以由一个应用程序代表(如Spring Authorization Server所实现的那样)。spring-doc.cadn.net.cn

例如,一个典型的基于 OAuth2 的微服务架构可能包含一个面向用户的客户端应用程序、多个提供 REST API 的后端资源服务器,以及一个用于管理用户和处理身份验证事宜的第三方授权服务器。 通常也会出现这样的情况:单个应用程序仅承担上述角色之一,而需要与一个或多个提供其他角色的第三方进行集成。spring-doc.cadn.net.cn

Spring Security 能处理这些场景及更多情况。 以下章节介绍了 Spring Security 提供的角色,并包含常见场景的示例。spring-doc.cadn.net.cn

OAuth2 资源服务器

本节包含 OAuth2 资源服务器功能的概要及示例。 完整的参考文档请参见OAuth 2.0 资源服务器spring-doc.cadn.net.cn

首先,将 spring-security-oauth2-resource-server 依赖项添加到您的项目中。 当使用 Spring Boot 时,请添加以下 starter:spring-doc.cadn.net.cn

使用 Spring Boot 的 OAuth2 客户端
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 Securityspring-doc.cadn.net.cn

考虑以下 OAuth2 资源服务器的使用场景:spring-doc.cadn.net.cn

使用 OAuth2 访问Tokens保护访问

使用 OAuth2 访问Tokens来保护 API 的访问是非常常见的。 在大多数情况下,Spring Security 只需要极少的配置即可使用 OAuth2 保护应用程序。spring-doc.cadn.net.cn

Spring Security 支持两种类型的 Bearer Tokens,每种Tokens使用不同的组件进行验证:spring-doc.cadn.net.cn

JWT 支持

以下示例使用 Spring Boot 配置属性来配置一个 JwtDecoder Bean:spring-doc.cadn.net.cn

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

使用 Spring Boot 时,仅需上述配置即可。 Spring Boot 提供的默认配置等同于以下内容:spring-doc.cadn.net.cn

使用 JWT 配置资源服务器
@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")
	}

}

非透明Tokens支持

以下示例使用 Spring Boot 配置属性来配置一个 OpaqueTokenIntrospector Bean:spring-doc.cadn.net.cn

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 提供的默认配置等同于以下内容:spring-doc.cadn.net.cn

使用不透明Tokens配置资源服务器
@Configuration
@EnableWebSecurity
public class SecurityConfig {

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

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

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

@Configuration
@EnableWebSecurity
class SecurityConfig {

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

		return http.build()
	}

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

}

使用自定义 JWT 保护访问安全

使用 JWT 来保护 API 访问是一种相当常见的目标,尤其是在前端以单页应用程序(SPA)形式开发时。 Spring Security 中的 OAuth2 资源服务器支持可用于任何类型的 Bearer Tokens,包括自定义的 JWT。spring-doc.cadn.net.cn

使用JWT保护API所需的一切是一个JwtDecoder Bean,该Bean用于验证签名和解码Tokens。 Spring Security会自动使用提供的Bean在SecurityFilterChain中配置保护。spring-doc.cadn.net.cn

以下示例使用 Spring Boot 配置属性来配置一个 JwtDecoder Bean:spring-doc.cadn.net.cn

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

您可以将公钥作为类路径资源提供(在此示例中名为 my-public-key.pub)。spring-doc.cadn.net.cn

使用 Spring Boot 时,仅需上述配置即可。 Spring Boot 提供的默认配置等同于以下内容:spring-doc.cadn.net.cn

使用自定义 JWT 配置资源服务器
@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 不提供用于生成Tokens的端点。 然而,Spring Security 确实提供了 JwtEncoder 接口以及一个实现类,即 NimbusJwtEncoderspring-doc.cadn.net.cn

OAuth2 客户端

本节包含 OAuth2 客户端功能的概述及示例。 完整的参考文档请参见 OAuth 2.0 客户端OAuth 2.0 登录spring-doc.cadn.net.cn

首先,将 spring-security-oauth2-client 依赖项添加到您的项目中。 当使用 Spring Boot 时,请添加以下 starter:spring-doc.cadn.net.cn

使用 Spring Boot 的 OAuth2 客户端
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 Securityspring-doc.cadn.net.cn

请考虑以下 OAuth2 客户端的使用场景:spring-doc.cadn.net.cn

使用 OAuth2 登录用户

通常需要用户通过 OAuth2 进行登录。 OpenID Connect 1.0 提供了一种特殊的Tokens,称为 id_token,其设计目的是让 OAuth2 客户端能够验证用户身份并实现用户登录。 在某些情况下,OAuth2 可直接用于用户登录(例如 GitHub 和 Facebook 等流行的社交登录提供商,它们并未实现 OpenID Connect)。spring-doc.cadn.net.cn

以下示例配置应用程序作为 OAuth2 客户端,能够使用 OAuth2 或 OpenID Connect 对用户进行登录:spring-doc.cadn.net.cn

配置 OAuth2 登录
@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 Bean 配置至少一个 ClientRegistrationRepository。 以下示例使用 Spring Boot 配置属性来配置一个 InMemoryClientRegistrationRepository Bean:spring-doc.cadn.net.cn

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

通过上述配置,应用程序现在支持两个额外的端点:spring-doc.cadn.net.cn

  1. 登录端点(例如 /oauth2/authorization/my-oidc-client)用于启动登录流程,并重定向到第三方授权服务器。spring-doc.cadn.net.cn

  2. 重定向端点(例如 /login/oauth2/code/my-oidc-client)由授权服务器用于重定向回客户端应用程序,其中将包含一个 code 参数,该参数用于通过访问Tokens请求获取 id_token 和/或 access_tokenspring-doc.cadn.net.cn

上述配置中包含 openid 范围表明应使用 OpenID Connect 1.0。 这会指示 Spring Security 在请求处理过程中使用 OIDC 特定的组件(例如 OidcUserService)。 如果没有此范围,Spring Security 将改用 OAuth2 特定的组件(例如 DefaultOAuth2UserService)。spring-doc.cadn.net.cn

访问受保护的资源

向受 OAuth2 保护的第三方 API 发起请求是 OAuth2 客户端的核心使用场景。 这通过授权一个客户端(在 Spring Security 中由 OAuth2AuthorizedClient 类表示)来实现,并在出站请求的 Bearer 头中放置一个 Authorization Tokens,以访问受保护的资源。spring-doc.cadn.net.cn

以下示例将应用程序配置为充当 OAuth2 客户端,能够向第三方 API 请求受保护的资源:spring-doc.cadn.net.cn

配置 OAuth2 客户端
@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())。 有关将 #oauth2-client-access-protected-resources-current-useroauth2Client() 结合使用的示例,请参见下一节spring-doc.cadn.net.cn

除了上述配置外,应用程序需要通过ClientRegistration Bean 配置至少一个 ClientRegistrationRepository。 以下示例使用 Spring Boot 配置属性来配置一个 InMemoryClientRegistrationRepository Bean:spring-doc.cadn.net.cn

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 提供了获取访问Tokens的 OAuth2AuthorizedClientManager 的实现,这些访问Tokens可以用来访问受保护的资源。spring-doc.cadn.net.cn

Spring Security 会为你注册一个默认的 OAuth2AuthorizedClientManager Bean,如果不存在的话。spring-doc.cadn.net.cn

使用 OAuth2AuthorizedClientManager 最简单的方式是通过一个 ClientHttpRequestInterceptor,该拦截器通过 RestClient 拦截请求,当 classpath 中包含 spring-web 时,该功能已可直接使用。spring-doc.cadn.net.cn

以下示例使用默认的 OAuth2AuthorizedClientManager 来配置一个 RestClient,该客户端能够通过在每个请求的 Bearer 头中放置 Authorization Tokens来访问受保护的资源:spring-doc.cadn.net.cn

使用 RestClient 配置 ClientHttpRequestInterceptor
@Configuration
public class RestClientConfig {

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

}
@Configuration
class RestClientConfig {

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

}

这个已配置的RestClient可按以下示例使用:spring-doc.cadn.net.cn

使用 RestClient 访问受保护的资源
import static org.springframework.security.oauth2.client.web.client.RequestAttributeClientRegistrationIdResolver.clientRegistrationId;

@RestController
public class MessagesController {

	private final RestClient restClient;

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

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

	public record Message(String message) {
	}

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

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

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

	data class Message(val message: String)

}

访问受保护的资源WebClient

向受 OAuth2 保护的第三方 API 发起请求是 OAuth2 客户端的核心使用场景。 这通过授权一个客户端(在 Spring Security 中由 OAuth2AuthorizedClient 类表示)来实现,并在出站请求的 Bearer 头中放置一个 Authorization Tokens,以访问受保护的资源。spring-doc.cadn.net.cn

以下示例将应用程序配置为充当 OAuth2 客户端,能够向第三方 API 请求受保护的资源:spring-doc.cadn.net.cn

配置 OAuth2 客户端
@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())。 有关将 #oauth2-client-access-protected-resources-current-useroauth2Client() 结合使用的示例,请参见上一节spring-doc.cadn.net.cn

除了上述配置外,应用程序需要通过ClientRegistration Bean 配置至少一个 ClientRegistrationRepository。 以下示例使用 Spring Boot 配置属性来配置一个 InMemoryClientRegistrationRepository Bean:spring-doc.cadn.net.cn

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 提供了获取访问Tokens的 OAuth2AuthorizedClientManager 的实现,这些访问Tokens可以用来访问受保护的资源。spring-doc.cadn.net.cn

Spring Security 会为你注册一个默认的 OAuth2AuthorizedClientManager Bean,如果不存在的话。spring-doc.cadn.net.cn

与其配置 RestClient,另一种使用 OAuth2AuthorizedClientManager 的方式是通过 ExchangeFilterFunction,它通过 WebClient 拦截请求。 要使用 WebClient,您需要添加 spring-webflux 依赖项以及一个响应式客户端实现:spring-doc.cadn.net.cn

添加 Spring WebFlux 依赖
implementation 'org.springframework:spring-webflux'
implementation 'io.projectreactor.netty:reactor-netty'
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-webflux</artifactId>
</dependency>
<dependency>
	<groupId>io.projectreactor.netty</groupId>
	<artifactId>reactor-netty</artifactId>
</dependency>

以下示例使用默认的 OAuth2AuthorizedClientManager 来配置一个 WebClient,该客户端能够通过在每个请求的 Bearer 头中放置 Authorization Tokens来访问受保护的资源:spring-doc.cadn.net.cn

使用 WebClient 配置 ExchangeFilterFunction
@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可按以下示例使用:spring-doc.cadn.net.cn

使用 WebClient 访问受保护的资源
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>()
			.block()!!
	}

	data class Message(val message: String)

}

访问当前用户的受保护资源

当用户通过 OAuth2 或 OpenID Connect 登录时,授权服务器可能会提供一个访问Tokens,该Tokens可直接用于访问受保护的资源。 这非常方便,因为只需配置一个 ClientRegistration 即可同时满足这两种使用场景。spring-doc.cadn.net.cn

本节将使用 OAuth2 登录用户访问受保护资源合并为单一配置。 还存在其他高级场景,例如为登录配置一个ClientRegistration,为访问受保护资源配置另一个3。 所有这些场景都使用相同的基本配置。spring-doc.cadn.net.cn

以下示例将应用程序配置为一个 OAuth2 客户端,能够登录用户向第三方 API 请求受保护的资源:spring-doc.cadn.net.cn

配置 OAuth2 登录和 OAuth2 客户端
@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 Bean 配置至少一个 ClientRegistrationRepository。 以下示例使用 Spring Boot 配置属性来配置一个 InMemoryClientRegistrationRepository Bean:spring-doc.cadn.net.cn

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

与前面的示例(使用 OAuth2 登录用户访问受保护资源)相比,本示例的主要区别在于通过 scope 属性所配置的内容,它将标准作用域 openidprofile 与自定义作用域 message.readmessage.write 结合在一起。spring-doc.cadn.net.cn

除了配置 Spring Security 以支持 OAuth2 Client 特性外,您还需要决定如何访问受保护的资源,并相应地进行配置。 Spring Security 提供了获取访问Tokens的 OAuth2AuthorizedClientManager 的实现,这些访问Tokens可以用来访问受保护的资源。spring-doc.cadn.net.cn

Spring Security 会为你注册一个默认的 OAuth2AuthorizedClientManager Bean,如果不存在的话。spring-doc.cadn.net.cn

使用 OAuth2AuthorizedClientManager 最简单的方式是通过一个 ClientHttpRequestInterceptor,该拦截器通过 RestClient 拦截请求,当 classpath 中包含 spring-web 时,该功能已可直接使用。spring-doc.cadn.net.cn

以下示例使用默认的 OAuth2AuthorizedClientManager 来配置一个 RestClient,该客户端能够通过在每个请求的 Bearer 头中放置 Authorization Tokens来访问受保护的资源:spring-doc.cadn.net.cn

使用 RestClient 配置 ClientHttpRequestInterceptor
@Configuration
public class RestClientConfig {

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

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

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

}
@Configuration
class RestClientConfig {

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

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

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

}

这个已配置的RestClient可按以下示例使用:spring-doc.cadn.net.cn

使用 RestClient 访问受保护的资源(当前用户)
@RestController
public class MessagesController {

	private final RestClient restClient;

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

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

	public record Message(String message) {
	}

}
import org.springframework.web.client.body

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

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

	data class Message(val message: String)

}

上一个示例不同,请注意我们无需告诉 Spring Security 要使用的 clientRegistrationId。 这是因为该值可以从当前登录的用户推导得出。spring-doc.cadn.net.cn

使用客户端凭证授权

本节重点介绍客户端凭证授权类型(client credentials grant type)的其他注意事项。 有关所有授权类型的通用设置和使用方法,请参阅访问受保护资源spring-doc.cadn.net.cn

客户端凭证授权允许客户端代表自身获取一个access_token。 客户端凭证授权是一种不涉及资源所有者(即用户)的简单流程。spring-doc.cadn.net.cn

需要注意的是,客户端凭证许可(client credentials grant)的典型用法意味着任何请求(或用户)都可能获取访问Tokens,并向资源服务器发起受保护资源的请求。 在设计应用程序时务必谨慎,确保用户无法发起未经授权的请求,因为每个请求都能够获取访问Tokens。spring-doc.cadn.net.cn

在用户可以登录的 Web 应用程序中获取访问Tokens时,Spring Security 的默认行为是为每个用户获取一个访问Tokens。spring-doc.cadn.net.cn

默认情况下,访问Tokens的作用域限定为当前用户的主体名称,这意味着每位用户都会获得一个唯一的访问Tokens。spring-doc.cadn.net.cn

使用客户端凭证授权(client credentials grant)的客户端通常需要将访问Tokens的作用域限定到应用程序本身,而不是单个用户,因此每个应用程序仅有一个访问Tokens。 为了将访问Tokens的作用域限定到应用程序,您需要设置一个策略来解析自定义的主体名称(principal name)。 以下示例通过使用 RestClient 配置一个 RequestAttributePrincipalResolver 来实现这一点:spring-doc.cadn.net.cn

RestClient 配置 client_credentials
@Configuration
public class RestClientConfig {

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

}
@Configuration
class RestClientConfig {

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

}

通过上述配置,可以为每个请求指定一个主体名称。 以下示例演示了如何通过指定主体名称,将访问Tokens的作用域限定到应用程序:spring-doc.cadn.net.cn

将访问Tokens的作用域限定到应用程序
import static org.springframework.security.oauth2.client.web.client.RequestAttributeClientRegistrationIdResolver.clientRegistrationId;
import static org.springframework.security.oauth2.client.web.client.RequestAttributePrincipalResolver.principal;

@RestController
public class MessagesController {

	private final RestClient restClient;

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

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

	public record Message(String message) {
	}

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

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

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

	data class Message(val message: String)

}

当通过属性指定主体名称(如上例所示)时,将仅存在一个访问Tokens,并且该Tokens将用于所有请求。spring-doc.cadn.net.cn

启用扩展授权类型

一个常见的使用场景涉及启用和/或配置扩展授权类型。 例如,Spring Security 提供了对 jwt-bearertoken-exchange 授权类型的支持,但默认情况下并未启用它们,因为这些授权类型不属于 OAuth 2.0 核心规范的一部分。spring-doc.cadn.net.cn

从 Spring Security 6.2 及更高版本开始,我们只需发布一个或多个 OAuth2AuthorizedClientProvider 的 Bean,它们将被自动识别并使用。 以下示例仅启用了 jwt-bearer 授权类型:spring-doc.cadn.net.cn

启用 jwt-bearer 授权类型
@Configuration
public class SecurityConfig {

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

}
@Configuration
class SecurityConfig {

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

}

Spring Security会在未提供自定义的 OAuth2AuthorizedClientManager 时自动发布一个默认的 1spring-doc.cadn.net.cn

任何自定义的 OAuth2AuthorizedClientProvider Bean 也会被自动识别,并在默认授权类型之后应用到所提供的 OAuth2AuthorizedClientManager 中。spring-doc.cadn.net.cn

为了在 Spring Security 6.2 之前实现上述配置,我们必须自行发布此 Bean,并确保同时重新启用默认的授权类型。 要了解幕后所配置的内容,以下是一个可能的配置示例:spring-doc.cadn.net.cn

启用 jwt-bearer 授权类型(6.2 版本之前)
@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
	}

}

自定义现有授权类型

The ability to 启用扩展授权类型通过发布一个bean,也提供了无需重新定义默认设置即可自定义现有授权类型的机遇。 例如,如果我们想为OAuth2AuthorizedClientProvider授权定制client_credentials的时间偏移量,我们可以简单地发布一个类似的bean:spring-doc.cadn.net.cn

自定义客户端凭证授权类型
@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
	}

}

自定义Tokens请求参数

在获取访问Tokens时,自定义请求参数的需求相当常见。 例如,假设我们希望在Tokens请求中添加一个自定义的 audience 参数,因为提供方要求在使用 authorization_code 授权模式时必须包含此参数。spring-doc.cadn.net.cn

从 Spring Security 6.2 及更高版本开始,我们只需发布一个类型为 OAuth2AccessTokenResponseClient 的 Bean,并指定泛型类型为 OAuth2AuthorizationCodeGrantRequest,Spring Security 就会使用它来配置 OAuth2 客户端组件。spring-doc.cadn.net.cn

以下示例在不使用 DSL 的情况下,为 authorization_code 授权类型自定义Tokens请求参数:spring-doc.cadn.net.cn

自定义授权码许可的Tokens请求参数
@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-doc.cadn.net.cn

在 Spring Security 6.2 之前,我们必须确保通过 Spring Security DSL 将此自定义配置同时应用于 OAuth2 登录(如果使用此功能)和 OAuth2 客户端组件。 为了理解幕后所配置的内容,以下是该配置可能的样子:spring-doc.cadn.net.cn

自定义授权码许可的Tokens请求参数(6.2 版本之前)
@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 授权类型的Tokens请求,我们可以发布如下 Bean:spring-doc.cadn.net.cn

为客户端凭证授权自定义Tokens请求参数
@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:spring-doc.cadn.net.cn

发布类型为OAuth2AccessTokenResponseClient<JwtBearerGrantRequest>的bean将会自动启用jwt-bearer授权类型,无需单独进行配置spring-doc.cadn.net.cn

发布类型为OAuth2AccessTokenResponseClient<TokenExchangeGrantRequest>的bean将会自动启用token-exchange授权类型,无需单独进行配置spring-doc.cadn.net.cn

自定义RestOperations被 OAuth2 客户端组件使用

另一个常见的使用场景是需要自定义用于获取访问Tokens(access token)的 RestOperations。 我们可能需要这样做,以便自定义响应处理(通过自定义的 HttpMessageConverter),或者为公司网络应用代理设置(通过自定义的 ClientHttpRequestFactory)。spring-doc.cadn.net.cn

从 Spring Security 6.2 及更高版本开始,我们只需发布类型为 OAuth2AccessTokenResponseClient 的 Bean,Spring Security 就会自动为我们配置并发布一个 OAuth2AuthorizedClientManager Bean。spring-doc.cadn.net.cn

以下示例为所有支持的授权类型自定义了 RestOperationsspring-doc.cadn.net.cn

为 OAuth2 客户端自定义 RestOperations
@Configuration
public class SecurityConfig {

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

		return accessTokenResponseClient;
	}

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

		return accessTokenResponseClient;
	}

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

		return accessTokenResponseClient;
	}

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

		return accessTokenResponseClient;
	}

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

		return accessTokenResponseClient;
	}

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

		return accessTokenResponseClient;
	}

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

}
@Configuration
class SecurityConfig {

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

		return accessTokenResponseClient
	}

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

		return accessTokenResponseClient
	}

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

		return accessTokenResponseClient
	}

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

		return accessTokenResponseClient
	}

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

		return accessTokenResponseClient
	}

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

		return accessTokenResponseClient
	}

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

}

Spring Security会在未提供自定义的 OAuth2AuthorizedClientManager 时自动发布一个默认的 1spring-doc.cadn.net.cn

请注意,在这种情况下,我们不需要自定义 SecurityFilterChain Bean,可以直接使用默认配置。 如果使用 Spring Boot 且没有其他自定义需求,我们实际上可以完全省略 SecurityFilterChain Bean。spring-doc.cadn.net.cn

在 Spring Security 6.2 之前,我们必须确保此自定义配置同时应用于 OAuth2 登录(如果使用此功能)和 OAuth2 客户端组件。 我们需要同时使用 Spring Security DSL(用于 authorization_code 授权类型)并发布一个类型为 OAuth2AuthorizedClientManager 的 Bean,以支持其他授权类型。 为了理解背后实际配置了什么内容,以下是一个可能的配置示例:spring-doc.cadn.net.cn

为 OAuth2 客户端自定义 RestOperations(6.2 版本之前)
@Configuration
@EnableWebSecurity
public class SecurityConfig {

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

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

		return http.build();
	}

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

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

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

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

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

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

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

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

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

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

		return authorizedClientManager;
	}

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

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

@Configuration
@EnableWebSecurity
class SecurityConfig {

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

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

		return http.build()
	}

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

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

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

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

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

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

		val tokenExchangeAuthorizedClientProvider = TokenExchangeOAuth2AuthorizedClientProvider()
		tokenExchangeAuthorizedClientProvider.setAccessTokenResponseClient(tokenExchangeAccessTokenResponseClient)

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

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

		return authorizedClientManager
	}

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

}

进一步阅读

前面的章节介绍了Spring Security对OAuth2的支持,并提供了常见场景的示例。 您可以在参考文档的相关部分阅读更多关于OAuth2客户端和资源服务器的内容:spring-doc.cadn.net.cn