Resource Server
这一课你将能做到
- 配置
spring-boot-starter-oauth2-resource-server,用 issuer-uri 自动发现 JWKS,校验传入的 Bearer access_token
- 区分 JWT 本地验签与 opaque token introspection 两种模式,知道何时选哪个
- 用
@PreAuthorize("hasAuthority('SCOPE_xxx')") 做 scope 级控制,并与本站自研 JWT 保护的路径分区共存
为什么要学
Client 负责拿票;Resource Server 负责验票。当你对外开放 API——第三方应用拿着授权服务器发的 access_token 来调你的 /api/v1/tracks——你的 backend 就切换成了 Resource Server 角色:验签、看 scope、执行业务。
它与上一课是同一枚硬币的两面(一个去别人那登录,一个验证别人的票),也和本站自研 JWT 的验签逻辑同源——区别只在发行方变成了标准 IdP、授权语言变成了 scope、验签公钥来自 IdP 的 JWKS 而不是你自己的密钥。
面试角度:「JWT 怎么验」「为什么用 issuer-uri 而不是手填 jwks-uri」「RS 与 Client 能否共存」都是开放 API 场景的常考题。
先建立图像
外部 Client(第三方应用)
│ Authorization: Bearer eyJ...
▼
你的 API(Resource Server)
│ ① 取 token:解析 Authorization 头,格式不对 → 401
│ ② 验签:用 iss 对应的 JWKS 公钥验签名,伪造/过期/aud 不符 → 401
│ ③ 看授权:scope/权限够不够?不够 → 403
│ ④ 执行 Controller
▼
200 业务成功
两种校验模式:
| 方式 | 适用 token | 特点 |
|---|
| JWT 本地验签 | JWT 形态的 access_token | 快,无网络往返;信任 iss + JWKS;撤销靠短寿命 |
| Introspection | 不透明(opaque)token | 每次询问授权服务器「这票有效吗、scope 是啥」;可加缓存 |
跟着做
步骤 1:加依赖
<!-- 中文注释:Resource Server 与 Client 可同时存在,但职责与配置完全不同 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
步骤 2:配置 issuer
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth.example.com
# Boot 启动时访问 {issuer-uri}/.well-known/openid-configuration
# 自动拿到 jwks_uri;也可显式配 jwk-set-uri 跳过发现
步骤 3:独立一条 Resource Server 链(与自研 JWT 链分区)
package com.example.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class ResourceServerSecurityConfig {
@Bean
public SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/v1/**") // 只管开放 API 前缀
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.GET, "/api/v1/public/**").permitAll()
.anyRequest().authenticated())
.oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()))
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
}
说明:/admin/** 等站内接口继续走你现有的自研 JWT 链;两条链按 securityMatcher 分区,互不越界。
步骤 4:按 scope 做方法级授权
// 中文注释:@EnableMethodSecurity 已在配置类开启;scope 转换为 SCOPE_ 前缀权限
@RestController
@RequestMapping("/api/v1/tracks")
public class TrackApiController {
@GetMapping
@PreAuthorize("hasAuthority('SCOPE_tracks:read')")
public List<Track> list() { /* ... */ }
@PostMapping
@PreAuthorize("hasAuthority('SCOPE_tracks:write')")
public Track create(@RequestBody Track body) { /* ... */ }
}
步骤 5:用 curl 观察 401 / 403
# ① 无 Bearer → 401,响应头带 WWW-Authenticate
curl -i http://localhost:8080/api/v1/tracks
# ② 有效 JWT 但缺 scope → 403
curl -i http://localhost:8080/api/v1/tracks \
-H "Authorization: Bearer VALID_BUT_NO_tracks:read_SCOPE"
# ③ 正常 → 200
curl -i http://localhost:8080/api/v1/tracks \
-H "Authorization: Bearer VALID_WITH_tracks:read"
步骤 6:不透明 token 改走 introspection
# 中文注释:IdP 发的是不透明串时,本地验不了签,必须远程询问
spring:
security:
oauth2:
resourceserver:
opaquetoken:
introspection-uri: https://auth.example.com/introspect
client-id: ${RS_INTROSPECT_CLIENT_ID}
client-secret: ${RS_INTROSPECT_CLIENT_SECRET}
配置后把链上的 oauth.jwt(...) 换成 oauth.opaqueToken(Customizer.withDefaults())。introspection 结果(active、scope、sub)可加本地缓存降低延迟。
讲清楚要点
- Resource Server 不发起授权跳转、不管登录页:它只消费 token——401/403 的语义要干净(401 未认证,403 权限不足)。
- JWT 本地验签的信任根是 issuer:
issuer-uri 决定 JWKS 从哪来、iss 声明怎么比对;配错一个字符,全部请求验签失败。
- 默认校验链不能关:签名、exp、nbf、iss 自动校验;aud 校验建议显式加上(自定义 JwtValidator),防止「发给别的应用的 token」混进来。
- JWT 撤销是短板,用短寿命补:本地验签意味着无法即时作废单个 token;开放 API 的 access 寿命控制在 15-60 分钟,撤销需求转嫁给 refresh/授权层。
- scope 不足是授权问题,返回 403:当成 401 处理会把第三方应用踢进重新授权死循环。
- 与本站自研 JWT 分区共存:
/api/v1/** 走 OAuth RS 链,/admin/** 走自研链;日志里标记 auth_kind=rs|site,排错先看路径再猜凭证种类。
你可能会懵的地方
-
同一个应用既是 Client 又是 Resource Server,矛盾吗?
不矛盾,是两种身份:对 GitHub 你是 Client(去登录);对第三方开发者你是 RS(开放 API)。两套配置、两条链、路径分区。
-
本站自研 JWT 和 OAuth 的 JWT 长得像,怎么区分?
看发行方与验签密钥:自研 JWT 用你自己的密钥签;OAuth access 用 IdP 的 JWKS。按路径分区后,每条链只认一种——绝不要写「什么都试一下」的万能验证器。
-
issuer-uri 配错的现象是什么?
启动期:无法拉取 /.well-known/openid-configuration 直接报错;运行期:验签全军覆没、全部 401。先 curl 该地址确认可达、域名拼写一致。
-
要不要自己校验 aud?
建议。默认校验不含 aud 时,「发给应用 A 的 token」可以被拿来调应用 B 的资源(只要签名有效)。加一个 JwtAudienceValidator 限定预期 aud。
-
高 QPS 下 introspection 会不会拖垮接口?
每请求一次远程调用确实贵。缓解:优先让 IdP 发 JWT(本地验签);必须用 opaque 时加短 TTL 缓存(如 30-60 秒),权衡撤销及时性。
错法对照
| 错法 | 问题 | 正解 |
|---|
| 只解析 JWT payload 不验签 | 伪造 token 畅通无阻 | issuer/JWKS 验签 + 声明校验 |
/admin 与 /api/v1 共用一条混沌鉴权链 | 两套凭证互相「碰巧能用」 | securityMatcher 分区 + 文档 |
| 缺 scope 返回 401 | 调用方误判未登录,死循环 | 已认证缺权限 → 403 |
| 把 id_token 当 API 的 Bearer | 受众错误,安全模型混乱 | API 只收 access_token |
| 生产上关掉 exp/iss 校验「先跑起来」 | 过期/跨站 token 全放行 | 默认校验链保持开启 |
卡点急救
- 现象:启动直接报 issuer 无法解析 / connection refused。
issuer-uri 地址不通或拼写错误。先 curl https://auth.example.com/.well-known/openid-configuration 确认可达且返回 JSON;本地 IdP 注意端口与容器网络。
- 现象:所有请求都 401,token 明明有效。排查四件套:服务器时钟(exp/nbf 全乱)、JWKS 缓存(IdP 换 key 未刷新)、alg 白名单(IdP 用 ES256 而你只放行 RS256)、securityMatcher 是否漏了该路径。
- 现象:接上 Resource Server 后 /admin 全 401。RS 链的
securityMatcher 写成了 /**,把管理接口也拦了。改回 /api/v1/**,并核对多条链的 @Order(更精确的 matcher 放前面)。
面试常问
-
JWT 本地验签和 token introspection 怎么选?
token 是 JWT 且追求低延迟 → 本地验签(信任 iss+JWKS,撤销靠短寿命);token 是不透明串或需要即时撤销语义 → introspection(每次问授权服务器,可加缓存)。本质是「性能 vs 即时撤销」的权衡。
-
Resource Server 验 JWT 的完整流程?
① 从 Authorization 头取 Bearer token;② 按 issuer 拉取/缓存 JWKS 验签;③ 校验 exp/nbf/iss(及自定义 aud);④ 解析 scope 映射为 SCOPE_ 权限;⑤ 走方法/路径授权;⑥ 全程无会话,STATELESS。
-
你们站自研 JWT 与 OAuth Resource Server 如何共存?
路径分区:站内接口走自研 JWT 链(自家密钥验签、角色鉴权),开放 API 前缀走 RS 链(IdP JWKS 验签、scope 鉴权);日志标记凭证种类,排错先看路径。两种凭证互不通用,也不会互相干扰。
自测三问
- JWT 本地验签与 introspection 各适合什么 token 形态?各自的撤销短板怎么补?
- 画出外部 Client 调用你
/api/v1/tracks 时的校验顺序与两个失败出口(401/403)。
- 为什么本站建议 admin 走自研 JWT,而开放 API 走 Resource Server?
小结一句话
Resource Server = 验 access_token(JWKS 验签或 introspection)、看 scope、再跑业务;与本站自研 JWT 链按路径分区、各管一段。
延伸