OAuth2 Resource Server
定义与作用
OAuth2 Resource Server(资源服务器) 是 OAuth2 架构中负责托管和保护用户资源的服务端点。它不信任任何直接来自客户端的请求,只认可由授权服务器(Authorization Server)颁发的有效 Access Token。当客户端携带 Token 请求资源时,资源服务器必须独立验证该 Token 的签名、有效期、发行方(issuer)以及权限范围(scope),然后决定是否放行。
对比自建用户体系的痛点
| 痛点 | 自建 Session 方案 | OAuth2 Resource Server 方案 |
|---|---|---|
| 分布式 Session 同步 | 多实例部署需引入 Redis / Spring Session 共享 Session,增加运维复杂度 | 无状态设计,Token 自身携带所有认证信息,任意实例均可独立校验 |
| 跨服务身份传递 | 服务间调用需伪造 Session 或传递 Cookie,安全性差 | 统一使用 JWT Token,任意服务均可通过公钥独立校验,无需信任中心 |
| 授权服务器耦合 | 认证逻辑与资源服务逻辑混在一起,难以拆分和扩展 | 认证交给授权服务器,资源服务器只关注"Token 是否有效 + 权限是否足够",职责单一 |
| 凭证过期策略 | 全局 Session 过期难以精细化控制 | 每个 JWT 自带 exp(过期时间)和 iat(签发时间),资源服务器可独立判断 Token 生命周期 |
Spring Security 5.x 通过 spring-boot-starter-oauth2-resource-server 模块提供开箱即用的 Resource Server 支持,核心组件包括 JwtDecoder(负责解码和校验 JWT)和 JwtAuthenticationConverter(负责将 JWT Claims 转换为 Spring Security 的 Authentication 对象)。
核心原理
Resource Server JWT 校验流程
issuer-uri 与 jwk-set-uri 的区别
| 配置项 | 作用 | 配置方式 | 适用场景 |
|---|---|---|---|
| issuer-uri | 指向授权服务器的 OpenID Connect 发现端点,Resource Server 自动从 /.well-known/openid-configuration 获取 jwks_uri 等元数据 | spring.security.oauth2.resourceserver.jwt.issuer-uri: https://auth.example.com | 授权服务器支持 OIDC 发现协议,希望自动获取配置 |
| jwk-set-uri | 直接显式配置 JSON Web Key Set(JWKS)端点地址,Resource Server 不再执行自动发现 | spring.security.oauth2.resourceserver.jwt.jwk-set-uri: https://auth.example.com/.well-known/jwks.json | 授权服务器不支持 OIDC 发现,或需要绕过自动发现以缩短启动时间 |
关键机制:NimbusJwtDecoder 获取 JWKS 后会缓存公钥,并在 Token 的 kid(Key ID)头部指示下选择对应的公钥进行签名验证。如果授权服务器轮换公钥,Decoder 会自动刷新 JWKS 缓存。
完整示例
示例一:基于 issuer-uri 的自动发现配置
场景说明:团队使用 Auth0 作为授权服务器,Spring Boot 应用作为资源服务器提供 REST API。Auth0 支持标准 OpenID Connect 发现协议,因此使用 issuer-uri 让 Spring Security 自动获取所有 JWT 校验所需的端点。
操作前配置(application.yml):
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://dev-abc123.auth0.com/
# 调试日志(开发环境)
logging:
level:
org.springframework.security.oauth2: DEBUG
操作后配置(Spring Security 5.x 配置类):
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // 无状态 API 通常禁用 CSRF
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeRequests(authz -> authz
.antMatchers("/api/public/**").permitAll()
.antMatchers(HttpMethod.GET, "/api/articles/**").hasAuthority("SCOPE_read:articles")
.antMatchers(HttpMethod.POST, "/api/articles/**").hasAuthority("SCOPE_write:articles")
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
);
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter =
new JwtGrantedAuthoritiesConverter();
// Auth0 默认将权限放在 "permissions" claim 中
grantedAuthoritiesConverter.setAuthoritiesClaimName("permissions");
grantedAuthoritiesConverter.setAuthorityPrefix("SCOPE_");
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
converter.setPrincipalClaimName("sub"); // 使用 JWT 的 sub 作为用户名
return converter;
}
}
结果分析:
- 应用启动时,
NimbusJwtDecoder自动向https://dev-abc123.auth0.com/.well-known/openid-configuration发送请求 - 从返回的 JSON 中提取
jwks_uri(如https://dev-abc123.auth0.com/.well-known/jwks.json) - 进一步请求 JWKS 端点,获取当前有效的公钥集合并缓存到内存
- 客户端携带
Authorization: Bearer <JWT>请求/api/articles/123 JwtDecoder解析 JWT Header 中的kid,从缓存的 JWKS 中找到匹配的公钥,验证 RSA 签名- 校验
iss是否等于https://dev-abc123.auth0.com/,exp是否未过期,aud是否包含当前应用 - 通过
JwtAuthenticationConverter将permissionsclaim 映射为SCOPE_read:articles等权限 AuthorizationFilter比对权限,发现GET /api/articles/123需要SCOPE_read:articles,匹配成功,放行请求
示例二:基于 jwk-set-uri 的显式配置(企业内网场景)
场景说明:某金融机构内部使用自建的 OAuth2 授权服务器,出于安全策略禁止应用访问外部自动发现端点,只允许白名单内的 JWKS 端点。需要显式配置 jwk-set-uri 并禁用自动发现。
操作前配置(application.yml):
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: https://internal-auth.bank.com/jwks/keys
# 显式配置 issuer 用于校验(可选但推荐)
issuer-uri: https://internal-auth.bank.com
# 配置连接池和超时(企业内网通常有防火墙限制)
server:
netty:
connection-timeout: 5000
操作后配置(Spring Security 5.x 配置类,含自定义 JwtDecoder 以配置缓存和超时):
@Configuration
@EnableWebSecurity
public class InternalResourceServerConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeRequests(authz -> authz
.antMatchers("/health", "/actuator/health").permitAll()
.antMatchers("/api/v1/accounts/**").hasAuthority("SCOPE_accounts.read")
.antMatchers("/api/v1/transfers/**").hasAuthority("SCOPE_transfers.write")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(Customizer.withDefaults())
);
}
@Bean
public JwtDecoder jwtDecoder() {
// 使用 NimbusJwtDecoder 直接配置 Nimbus 连接参数
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(
"https://internal-auth.bank.com/jwks/keys"
).restOperations(restOperations()).build();
// 配置 JWT 校验策略:issuer, audience, 时间校验
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(
"https://internal-auth.bank.com"
);
OAuth2TokenValidator<Jwt> withAudience = AudienceValidator.of("account-service");
OAuth2TokenValidator<Jwt> validator = DelegatingOAuth2TokenValidator
.withDefaults(List.of(withIssuer, withAudience));
jwtDecoder.setJwtValidator(validator);
return jwtDecoder;
}
@Bean
public RestOperations restOperations() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(Duration.ofSeconds(5));
factory.setReadTimeout(Duration.ofSeconds(5));
return new RestTemplate(factory);
}
}
// 自定义 Audience 校验器
public class AudienceValidator implements OAuth2TokenValidator<Jwt> {
private final String audience;
private AudienceValidator(String audience) {
this.audience = audience;
}
public static AudienceValidator of(String audience) {
return new AudienceValidator(audience);
}
@Override
public OAuth2TokenValidatorResult validate(Jwt token) {
List<String> audiences = token.getAudience();
if (audiences != null && audiences.contains(audience)) {
return OAuth2TokenValidatorResult.success();
}
return OAuth2TokenValidatorResult.failure(
new OAuth2Error("invalid_token", "Audience mismatch", null)
);
}
}
结果分析:
- 应用启动时直接请求
https://internal-auth.bank.com/jwks/keys,不再访问/.well-known/openid-configuration - 启动速度快于
issuer-uri方式(减少一次 HTTP 往返),且完全符合企业网络白名单策略 - 自定义
RestOperations设置 5 秒超时,防止 JWKS 端点不可用时无限阻塞启动线程 - 自定义
AudienceValidator确保 Token 的audclaim 包含account-service,防止其他业务的 Token 误访问账户服务 - 当授权服务器轮换签名密钥时,带有旧
kid的 JWT 首次访问会触发 JWKS 缓存刷新,自动获取新公钥 - 若 Token 过期或签名无效,Resource Server 返回
401 Unauthorized,响应体中可能包含WWW-Authenticate: Bearer error="invalid_token"头
易错场景与面试考点
易错场景:issuer-uri 末尾缺少或多余斜杠导致启动失败
issuer-uri 在 Spring Security 5.x 中严格用于匹配 JWT 的 iss claim。配置时若末尾缺少 /,而授权服务器发放的 JWT 中 iss 包含尾部斜杠,会导致 JwtDecoder 在启动时验证失败。
错误配置:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth.example.com # 缺少尾部 /
JWT 中的 iss claim:
{
"iss": "https://auth.example.com/",
"sub": "user123",
"aud": ["my-app"],
"exp": 1718000000
}
错误日志(启动时):
Caused by: java.lang.IllegalStateException:
The Issuer "https://auth.example.com/" provided in the configuration
did not match the issuer "https://auth.example.com" from the well-known endpoint
正确做法:配置 issuer-uri 时严格与授权服务器的 issuer 字段保持一致。如果不确定,优先使用 jwk-set-uri 直接配置,或先在浏览器中访问 /.well-known/openid-configuration 确认 issuer 字段的确切值。
面试考点:
- 面试官常问:如果 Resource Server 启动时授权服务器的 JWKS 端点不可用,应用会怎样?
- 答:Spring Security 5.x 中
NimbusJwtDecoder在首次获取 JWKS 时若失败,启动过程会抛出异常导致应用无法启动。生产环境中建议:① 使用jwk-set-uri并在配置阶段配置重试机制;② 或实现自定义JwtDecoder延迟加载公钥;③ 通过 Spring Boot 的fail-fast=false与自定义BeanPostProcessor做降级处理。若应用已启动后 JWKS 端点短暂不可用,已缓存的公钥仍可用于校验 Token,直到缓存过期。
版本说明:本节基于 Spring Security 5.x 编写。5.x 中
WebSecurityConfigurerAdapter是标准基类,antMatchers用于路径匹配。若升级到 6.x,需改用SecurityFilterChainBean 和requestMatchers。