OAuth2基础
定义与作用
OAuth2(Open Authorization 2.0) 是一种行业标准的授权协议,允许第三方应用在不暴露用户密码的前提下,代表用户获取对特定资源的有限访问权限。它通过引入"授权服务器"作为中间层,将用户身份认证与资源访问授权解耦,解决了传统自建用户体系中的多个核心痛点。
对比自建用户体系的痛点
| 痛点 | 自建用户体系 | OAuth2方案 |
|---|---|---|
| 密码泄露风险 | 用户需将密码直接交给第三方应用,存在泄密隐患 | 用户仅在授权服务器输入密码,第三方应用永远接触不到密码 |
| 权限粒度控制 | 要么全部开放,要么拒绝访问,无法精确控制数据范围 | 通过 scope 机制限定权限范围(如仅读取头像、邮箱) |
| 凭证撤销困难 | 用户无法在不改密码的情况下单独收回某个第三方应用的权限 | 在授权服务器撤销特定 Token 即可,不影响其他应用 |
| 多平台账号管理 | 每个应用都需要独立的注册、登录、找回密码流程 | 复用已有授权服务器账号(如 Google、GitHub),降低用户流失率 |
Spring Security 从 5.x 起内置了完整的 OAuth2 支持,包括 OAuth2 Client(客户端)、OAuth2 Resource Server(资源服务器)和 OAuth2 Authorization Server(授权服务器,5.x 推荐外部方案如 Keycloak)。
核心原理
四大核心角色
| 角色 | 职责 | 对应 Spring Security 组件 |
|---|---|---|
| Resource Owner | 拥有受保护资源的用户,决定是否授权客户端访问 | 最终用户,对应 Authentication.getPrincipal() |
| Client | 代表用户发起资源访问请求的应用程序 | OAuth2Client / ClientRegistration |
| Authorization Server | 负责用户认证、授权决策、颁发 Token | 外部身份提供商(如 Google、GitHub)或 Keycloak |
| Resource Server | 托管受保护资源,负责校验 Token 并返回资源 | OAuth2ResourceServer + JwtDecoder |
四种授权模式
Spring Security 5.x 主要支持以下四种 OAuth2 授权模式,其中 Authorization Code(授权码模式) 和 Client Credentials(客户端凭证模式) 是生产环境最常用的两种:
| 授权模式 | 适用场景 | 安全性 | Spring Security 5.x 支持 |
|---|---|---|---|
| Authorization Code | 服务端 Web 应用、第三方登录 | 高(Token 不暴露给浏览器) | ✅ 核心支持 |
| Client Credentials | 服务间 API 调用、后台任务 | 高(无用户参与) | ✅ 核心支持 |
| Implicit(隐式) | 纯前端 SPA(已弃用) | 低(Token 暴露在 URL) | ⚠️ 不推荐 |
| Password(密码) | 高度信任的第一方应用(已弃用) | 中(需暴露密码) | ⚠️ 不推荐 |
Authorization Code 流程(授权码模式)
关键安全设计:
code只能使用一次,且有有效期(通常 10 分钟)client_secret仅在服务端与授权服务器之间传输,浏览器不可见state参数防止 CSRF 攻击,客户端必须验证回调的state与发送时一致
Client Credentials 流程(客户端凭证模式)
核心特点:无用户参与,客户端直接用自己的凭证换取 Token,适用于微服务间调用、定时任务等机器对机器(M2M)场景。
完整示例
示例一:Authorization Code 模式实现第三方登录
场景说明:企业内部管理系统需要支持员工使用公司统一身份认证平台(OIDC/OAuth2)登录,避免维护两套账号密码。
操作前配置(application.yml):
spring:
security:
oauth2:
client:
registration:
corporate-idp:
client-id: internal-app-client-id
client-secret: internal-app-client-secret
client-authentication-method: basic
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
scope: openid, profile, email
client-name: Corporate SSO
provider:
corporate-idp:
authorization-uri: https://sso.company.com/oauth/authorize
token-uri: https://sso.company.com/oauth/token
user-info-uri: https://sso.company.com/oauth/userinfo
user-name-attribute: sub
jwk-set-uri: https://sso.company.com/.well-known/jwks.json
操作后配置(Spring Security 5.x 配置类):
@Configuration
@EnableWebSecurity
public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authz -> authz
.antMatchers("/", "/public/**", "/login/**").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/dashboard", true)
.failureUrl("/login?error=true")
)
.logout(logout -> logout
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
);
}
}
结果分析:
- 用户访问
/dashboard时未登录,被重定向到/login页面 - 用户点击"Corporate SSO 登录",浏览器被重定向到
https://sso.company.com/oauth/authorize?response_type=code&client_id=internal-app-client-id&redirect_uri=...&scope=openid%20profile%20email&state=... - 用户在统一认证平台完成登录并授权,携带
code和state回调到/login/oauth2/code/corporate-idp - Spring Security 自动用
code+client_secret向token-uri换取access_token和id_token - 根据
user-info-uri获取用户信息,构造OAuth2User并存入SecurityContextHolder - 用户以
ROLE_USER身份访问/dashboard,认证成功
示例二:Client Credentials 模式实现服务间调用
场景说明:订单服务(Order Service)需要调用库存服务(Inventory Service)的 API 查询库存,两个服务之间通过 OAuth2 Client Credentials 模式进行安全认证。
操作前配置(库存服务作为 Resource Server,application.yml):
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth.company.com
操作前配置(订单服务作为 Client,application.yml):
spring:
security:
oauth2:
client:
registration:
inventory-api:
client-id: order-service-client-id
client-secret: order-service-client-secret
client-authentication-method: basic
authorization-grant-type: client_credentials
scope: inventory.read
token-uri: https://auth.company.com/oauth/token
操作后配置(订单服务发起调用):
@Service
public class InventoryClientService {
private final WebClient webClient;
public InventoryClientService(
OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth2.setDefaultClientRegistrationId("inventory-api");
this.webClient = WebClient.builder()
.filter(oauth2)
.baseUrl("https://inventory.company.com")
.build();
}
public Mono<InventoryResponse> checkStock(String productId) {
return webClient.get()
.uri("/api/inventory/{productId}", productId)
.retrieve()
.bodyToMono(InventoryResponse.class);
}
}
@Configuration
public class OAuth2ClientConfig {
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
}
结果分析:
- 订单服务调用库存 API 时,
WebClient过滤器自动检查OAuth2AuthorizedClientRepository中是否存在有效 Token - 若 Token 不存在或已过期,过滤器自动用
client_credentials向https://auth.company.com/oauth/token请求新 Token - 请求体携带
grant_type=client_credentials&client_id=order-service-client-id&client_secret=order-service-client-secret&scope=inventory.read - 获取
access_token后,自动在请求头中附加Authorization: Bearer <token> - 库存服务(Resource Server)收到请求后,使用
JwtDecoder校验 Token 签名、issuer、过期时间,并检查scope是否包含inventory.read - 校验通过返回库存数据,全程无用户参与,实现安全的机器对机器通信
易错场景与面试考点
易错场景:Authorization Code 回调阶段 state 参数不匹配导致登录失败
在 Authorization Code 流程中,Spring Security 5.x 的 OAuth2AuthorizationRequestRedirectFilter 会在重定向用户到授权服务器前生成一个随机的 state 参数并存入 HttpSessionOAuth2AuthorizationRequestRepository。当用户完成授权后回调时,如果回调请求中的 state 与 Session 中存储的不一致,Spring Security 会抛出 OAuth2AuthenticationException 并拒绝登录。
常见触发原因:
- 多实例部署时未配置 Session 共享(Sticky Session 失效导致请求落到另一台实例)
- 客户端在授权过程中意外清除了 Cookie 或 Session
- 自定义
OAuth2AuthorizationRequestRepository时未正确实现state的保存与比对
调试方法:
// 在配置类中开启 OAuth2 登录调试日志
logging:
level:
org.springframework.security.oauth2: DEBUG
查看日志中是否有 Authorization request state not matching 或 Missing state parameter 关键字。解决多实例问题需配置 Spring Session Redis 或基于数据库的 OAuth2AuthorizationRequestRepository 实现。
面试考点:
- 面试官常问:OAuth2 的
state参数和PKCE(Proof Key for Code Exchange)有什么区别? - 答:
state用于防止 CSRF 攻击,确保回调请求确实是当前用户发起的;PKCE是授权码模式的扩展,在公共客户端(如移动端、SPA)中防止授权码被拦截后盗用,通过code_challenge和code_verifier机制确保只有原始请求者能换取 Token。
版本说明:本节基于 Spring Security 5.x 编写。Spring Security 5.x 中
WebSecurityConfigurerAdapter和antMatchers是标准配置方式。若迁移到 6.x,需将WebSecurityConfigurerAdapter改为SecurityFilterChainBean,并将antMatchers替换为requestMatchers。