Spring Security OAuth 2 客户端配置
Spring Security OAuth 2 客户端配置完全指南
在微服务架构和第三方登录横行的今天,OAuth 2.0 已成为授权协议的事实标准。Spring Security 对其提供了深度支持,特别是客户端模式,让我们的应用能够安全地接入认证服务器并访问受保护资源。本教程将手把手带你从零开始,配置一个 OAuth 2 客户端,重点讲解最新 Spring Security 6.x 的最佳实践。
1. OAuth 2 客户端核心概念
在开始编码前,我们需要明确三种角色的区别:
- 资源所有者:通常是用户,拥有受保护资源(如头像、数据)。
- 客户端:你的 Spring Boot 应用,需要代表用户或自身去访问资源。
- 资源服务器:托管受保护资源的服务器,通过令牌验证请求。
- 授权服务器:认证用户并颁发令牌的服务器(如 Keycloak、Auth0、自建 Spring Authorization Server)。
本教程聚焦于客户端的配置,即你的应用如何从授权服务器获取令牌,并使用令牌去调用资源服务器的 API。主要涉及的授权模式为 授权码模式(Authorization Code) 和 客户端凭证模式(Client Credentials)。
2. 项目环境准备
创建一个标准的 Spring Boot 项目,记得添加以下关键依赖。
Maven 依赖 (pom.xml):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<!-- 如果准备使用 WebClient 进行令牌中继调用 -->
</dependency>
spring-boot-starter-oauth2-client 是核心,它会自动引入 Spring Security 及 OAuth 2 客户端所需的所有库。如果你需要实现“客户端凭证”模式并通过 WebClient 无缝带上令牌,还需要 spring-boot-starter-webflux。
3. 客户端注册信息配置 (application.yml)
Spring Security 通过 ClientRegistration 来表示一个已注册的客户端。在 application.yml 中配置是最简单的方式。我们以连接 GitHub 作为授权服务器为例(授权码模式)。
spring:
security:
oauth2:
client:
registration:
github:
client-id: your-github-client-id
client-secret: your-github-client-secret
scope:
- read:user
- user:email
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
authorization-grant-type: authorization_code
provider:
github:
authorization-uri: https://github.com/login/oauth/authorize
token-uri: https://github.com/login/oauth/access_token
user-info-uri: https://api.github.com/user
user-name-attribute: login
3.1 关键属性解释
| 配置项 | 说明 |
|---|---|
registration.[id] |
客户端唯一标识,例如 github,将用于自动生成回调地址。 |
client-id / client-secret |
在授权服务器注册应用时获得的凭据。 |
scope |
请求的权限范围,多个用逗号或 YAML 列表。 |
redirect-uri |
授权服务器回调地址,{baseUrl} 占位符代表当前应用地址,{registrationId} 就是 github。默认模板值正好是 /login/oauth2/code/{registrationId},可省略不写。 |
authorization-grant-type |
授权模式,默认是 authorization_code。 |
provider.[id] |
指定授权服务器元信息。Spring Security 内置了 Google、GitHub、Facebook 等常用提供商的默认配置,如果 provider 的 id 与已知提供商匹配,则无需手动填写 authorization-uri 等。但我们仍可像上面那样显式覆盖。 |
对于客户端凭证模式,配置会略有不同,后面会具体说明。
4. 安全配置:启用 OAuth2 登录
简单的安全配置类,融入 OAuth2 登录支持。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/", "/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.defaultSuccessUrl("/dashboard", true)
);
return http.build();
}
}
oauth2Login() 方法会自动添加一个 OAuth2LoginAuthenticationFilter,拦截 /login/oauth2/code/* 的回调请求,完成用户认证。用户首次访问受保护页面时,会被重定向到授权服务器的登录页面。成功登录后,应用会跳转到 /dashboard。
4.1 获取认证后的用户信息
你可以通过注入 OAuth2AuthorizedClientService 来获取令牌,也可以直接从 OAuth2User 中读取授权服务器返回的用户属性。
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
public class UserController {
@GetMapping("/user")
public Map<String, Object> user(@AuthenticationPrincipal OAuth2User principal) {
return principal.getAttributes();
}
}
5. OAuth2 客户端与服务间调用 (令牌中继)
OAuth2 客户端最常见的需求是:代表当前登录用户去调用另一个受保护的资源服务器。这就叫令牌中继(Token Relay)。Spring 提供 ServletOAuth2AuthorizedClientExchangeFilterFunction 来让 WebClient 自动携带当前的访问令牌。
5.1 配置 WebClient Bean
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
public class WebClientConfig {
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.clientCredentials()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
@Bean
public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
// 设置默认的 OAuth2 客户端注册id (如果在请求时不显式指定)
oauth2Client.setDefaultOAuth2AuthorizedClient(true);
oauth2Client.setDefaultClientRegistrationId("github");
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
}
这个配置的核心是 OAuth2AuthorizedClientManager,它会自动管理令牌的获取、刷新等。我们将其注入给 ServletOAuth2AuthorizedClientExchangeFilterFunction,然后应用到 WebClient 上。oauth2Client.setDefaultOAuth2AuthorizedClient(true) 表示如果请求中没有明确指定,则尝试从当前登录的用户上下文中获取 github 客户端对应的令牌。
5.2 发起远程调用
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
@RestController
public class ResourceController {
private final WebClient webClient;
public ResourceController(WebClient webClient) {
this.webClient = webClient;
}
@GetMapping("/repos")
public String repos() {
return this.webClient
.get()
.uri("https://api.github.com/user/repos")
.retrieve()
.bodyToMono(String.class)
.block();
}
}
此时访问 /repos,WebClient 会自动在 Authorization: Bearer xxx 请求头中附加当前用户的 GitHub 访问令牌。整个过程对开发者透明,极其方便。
6. 客户端凭证模式配置 (机器对机器)
如果你的应用并非代表用户,而是以自己的身份去访问资源(例如后台服务调用),那么需要使用 客户端凭证模式。此时没有用户上下文,配置与授权码模式略有不同。
6.1 application.yml 添加新注册
spring:
security:
oauth2:
client:
registration:
my-backend-api:
client-id: backend-client
client-secret: secret
authorization-grant-type: client_credentials
scope: read,write
provider: my-auth-server
provider:
my-auth-server:
token-uri: https://auth.example.com/oauth2/token
注意 authorization-grant-type: client_credentials,并且不需要 redirect-uri、authorization-uri 等。
6.2 使用 WebClient 以客户端自身名义调用
在前面的 WebClientConfig 中,OAuth2AuthorizedClientProviderBuilder 已经 .clientCredentials(),所以管理器已经支持该模式。我们只需在调用时显式指定使用哪个客户端注册,并且告知无需用户主体。
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
@RestController
public class MachineController {
private final WebClient webClient;
private final OAuth2AuthorizedClientManager authorizedClientManager;
public MachineController(WebClient webClient,
OAuth2AuthorizedClientManager authorizedClientManager) {
this.webClient = webClient;
this.authorizedClientManager = authorizedClientManager;
}
@GetMapping("/machine-data")
public String getData() {
// 手动获取令牌并手动设置到请求头,或者使用另一种更简洁的方法:
// 借助 ServletOAuth2AuthorizedClientExchangeFilterFunction 的快捷方式
// 但需要 ClientRequest,这里展示通用方法
String token = authorizedClientManager.authorize(
OAuth2AuthorizeRequest.withClientRegistrationId("my-backend-api")
.principal("backend") // 任意唯一名称,代表客户端自身
.build())
.getAccessToken().getTokenValue();
return webClient.get()
.uri("https://resource-server.com/api/data")
.headers(h -> h.setBearerAuth(token))
.retrieve()
.bodyToMono(String.class)
.block();
}
}
不过更好的方式是直接在 WebClient 调用中传入 attributes 来触发自动获取令牌。只需在请求前设置属性:
// 在 WebClient 调用的基础上添加 .attributes(...)
webClient.get()
.uri("https://resource-server.com/api/data")
.attributes(ServletOAuth2AuthorizedClientExchangeFilterFunction
.clientRegistrationId("my-backend-api"))
.retrieve()
.bodyToMono(String.class)
.block();
这样 ServletOAuth2AuthorizedClientExchangeFilterFunction 会检测到你需要 my-backend-api 的令牌,如果当前没有用户上下文,它会自动使用客户端凭证模式获取令牌并附加。记住,在这个场景下调用时不能有已认证的用户上下文,否则可能会意外使用用户令牌。
7. 常见问题与调试
- 重定向 URI 不匹配:确保在授权服务器上注册的回调地址与 application.yml 中的
redirect-uri完全一致(可带通配符)。本地开发时通常是http://localhost:8080/login/oauth2/code/github。 - 令牌不自动刷新:确保
OAuth2AuthorizedClientProvider链中包含了.refreshToken(),并且授权服务器支持刷新令牌。 - Spring Security 6 变化:旧的
AuthorizationRequestRepository等 API 有所变化,遵循本文的配置可完全兼容 Spring Boot 3.x / Security 6.x。 - CSRF 保护:如果你提供的是纯 API 服务,传统表单提交不多,可以在安全配置中禁用 CSRF:
http.csrf(AbstractHttpConfigurer::disable),但请谨慎评估。
8. 总结
通过以上步骤,你已经完成了 Spring Security OAuth 2 客户端的主要配置:
- 引入依赖,配置客户端注册信息。
- 启用
oauth2Login登录,获取用户身份。 - 配置支持令牌中继的
WebClient,代表用户调用下游服务。 - 配置客户端凭证模式,实现服务间的直接令牌获取。 这套模式足以应对绝大多数现代应用的安全需求。借助 Spring Security 的高度抽象,你无需手动处理令牌存储、刷新等繁琐细节,从而更专注于业务逻辑的开发。