在 JWT 一次讲透 中我们讨论了 JWT 的签名和加密机制。但 JWT 最根本的结构性问题不是算法选择,而是吊销:一旦签发,JWT 在到期之前就是一台按规则行驶的无人列车——服务端没有办法让它提前停下。
这个问题不是 JWT 特有的——任何无状态或近似无状态的令牌机制都面临同样的挑战。本文从吊销的场景分类出发,拆解四种主流吊销机制(Token Introspection、Refresh Token Rotation、吊销列表、基于事件的通知),给出工程权衡矩阵。
一、吊销的场景分类
| 场景 | 紧急程度 | 影响范围 | 需要吊销的令牌 |
|---|---|---|---|
| 用户修改密码 | 高 | 所有设备 | 所有该用户的 Access Token + Refresh Token |
| 管理员禁用账号 | 高 | 该用户 | 所有该用户的 Access Token + Refresh Token |
| 权限变更(降级/换岗) | 高 | 该用户 | Access Token(好让下次获取时缩小 scope) |
| 令牌泄露 | 最高 | 单一令牌 | 泄露的那一个 Refresh Token |
| 用户主动登出 | 低 | 当前设备 | 当前 Session + 对应的 Refresh Token |
| 设备丢失 | 高 | 该设备 | 该设备上的所有令牌 |
场景分析:修改密码和吊销令牌看起来是两件事,但在 JWT 体系下它们是同一个问题——旧的 JWT 不会因为密码改了而自动失效。密码修改后攻击者如果持有修改前的 JWT,仍可访问。
二、Token Introspection(RFC 7662)
Token Introspection 是最直接的吊销方案:RP 每次收到 Access Token 时,不去做本地 JWT 验证,而是把 token 发回 OP 的 Introspection Endpoint,由 OP 告诉 RP “这个 token 还有效吗”。
POST /introspect
Authorization: Basic <client_id:client_secret>
Content-Type: application/x-www-form-urlencoded
token=eyJhbG...access_token&token_type_hint=access_token
→ 200 OK
{
"active": true,
"scope": "read write",
"client_id": "my-app",
"username": "zhangsan",
"exp": 1718400000,
"sub": "user-12345"
}
→ 如果已吊销:
{
"active": false
}
优点:实时吊销——OP 可以在每次 Introspection 请求时决定 token 是否有效。
代价: - 每次 API 调用都需要向 OP 发起一次网络请求——延迟增加 5-50ms。 - OP 需要承受所有 RP 的所有 API 流量的 Introspection 请求量。 - 需要缓存层:如果对每个 Introspection 查询都做数据库查询,OP 会很快成为瓶颈。
实际使用:绝大多数实现不会对每个 API 请求都做 Introspection,而是结合本地 JWT 验证(短期 Access Token,5-15 分钟)加定期 Introspection(如每 5 分钟一次)。这形成了一种妥协:吊销窗口最长 5 分钟,但 99% 的请求不需要 Introspection 调用。
Token Introspection 和 JWT 的
jti(JWT ID)字段配合使用效果最好——OP 签发 JWT 时写jti,Introspection 时用jti快速索引到令牌的状态记录。
三、Refresh Token Rotation
在 OAuth 2.1 篇 中已经介绍了 Refresh Token Rotation 的基本原理——每次用 Refresh Token 换新 Access Token 时,OP 返回新的 Refresh Token,旧的立即失效。这里补充工程实现细节:
3.1 Token Family 模型
Rotation 的正确实现不是简单地”每次发新的,旧的失效”,而是维护一个 Refresh Token Family:
Token Family: RT-FAMILY-ABC
RT-1 (parent) → 签发 RT-2, 使 RT-1 失效
RT-2 (child of RT-1) → 签发 RT-3, 使 RT-2 失效
RT-3 (child of RT-2) → ...
如果一个 Refresh Token 被重放(已经被用过了): - 合法客户端持有 RT-3,攻击者持有 RT-1。 - 合法客户端正常使用 RT-3 → RT-4。 - 攻击者使用 RT-1 → RT-1 已经失效(它签发过 RT-2)。 - 此时系统检测到:RT-1 属于 FAMILY-ABC,但其直接后继 RT-2 早已签发——这不是”正常重放”,这是 FAMILY-ABC 中的某个历史 token 被窃取了。 - OP 应该使整个 Token Family 失效(所有 RT-1、RT-2、RT-3、RT-4 全部吊销),因为无法确定攻击者拿到了 family 中的哪些 token。
这称为”Automatic Replay Detection”——它提供了比简单 Rotation 更精确的泄露检测。
3.2 Refresh Token 的存储结构
在 OP 侧,每个 Refresh Token 至少存储:
| 字段 | 用途 |
|---|---|
token_hash |
Refresh Token 的哈希(用于索引和查询,不存明文) |
family_id |
Token Family 的唯一标识 |
parent_hash |
此 token 是从哪个 parent 签发出来的 |
user_id + client_id |
关联用户和客户端 |
scope |
此 token 可恢复的权限范围 |
created_at |
签发时间 |
expires_at |
过期时间 |
revoked |
是否已吊销 |
device_info |
可选的设备指纹(用于用户查看”哪些设备登录中”) |
数据库查询模式:SELECT * FROM refresh_tokens WHERE token_hash = $1,单行查询,通过
token_hash 的 B-Tree 索引主键查找实现。
四、吊销列表
对于不需要 Introspection 的场景(如只需要知道”哪些 token 已被吊销”,而不想知道 token 当前状态),可以使用吊销列表。
本地的吊销列表是一个简单的 Set / Bloom
Filter,存储已吊销的 JWT 的 jti:
吊销列表(Redis / 本地内存):
SET revoked_jti: "abc-123", "def-456", "ghi-789"
Access Token 验证流程:
1. 验证 JWT 签名
2. 检查 exp, iss, aud
3. 检查 jti 是否在吊销列表中
4. 如果在 → 拒绝请求
Bloom Filter 方案适合对内存敏感的部署(如 sidecar 模式下的 Envoy 插件)——允许一定的假阳性(可能拒绝少量合法的已吊销 token,但绝不会放过已吊销的 token)。
工程取舍:吊销列表的维护成本与吊销事件频率成正比。在令牌泄露或大批量员工离职的场景下,吊销列表可能迅速膨胀。需要定期清理过期 token 的
jti(exp 已过的 token 不需要被吊销——它们本来就是无效的)。
五、基于事件的吊销通知
以上三种方案都是”RP 去问”(Pull 模式)。基于事件的吊销是”OP 主动通知”(Push 模式)。
OpenID Shared Signals Framework (SSF) 和 CAEP(Continuous Access Evaluation Protocol)定义了这个机制:
- RP 订阅 OP 的安全事件流(SET,Security Event Token)。
- 当 OP 检测到安全事件(如”用户密码被重置”“会话被终止”“设备被撤销”),OP 向订阅的 RP 发送 SET。
- RP 收到 SET 后,立即使该用户的所有相关 token 失效。
{
"iss": "https://idp.example.com",
"iat": 1718396400,
"jti": "set-event-abc-123",
"aud": "https://rp.example.com",
"events": {
"urn:ietf:params:SCIM:event:passwordReset": {
"subject": {
"subject_type": "iss-sub",
"iss": "https://idp.example.com",
"sub": "user-12345"
}
}
}
}优点:延迟最小(事件发生后秒级推送),覆盖面最广。
缺点:依赖 OP 和 RP 双方实现 SSF/CAEP——2026 年这个标准仍处于早期采用阶段。Google 和 Microsoft 有内部使用的等效机制(Google 的 Continuous Access Evaluation),但标准化和跨产品互操作还在推进。
六、四种机制的工程权衡
| 机制 | 吊销延迟 | 性能影响 | 实现复杂度 | 覆盖范围 |
|---|---|---|---|---|
| Token Introspection | 实时(OP 被查询时) | 高(每次 API 调用多一次网络 RTT) | 低(标准 RFC) | Access Token |
| Refresh Token Rotation | 等于 Access Token 有效期 | 低(只在 refresh 时触发) | 中(需要实现 Token Family) | Refresh Token + 间接限制 Access Token |
| 本地吊销列表 | 等于列表更新频率 | 低(本地内存查询) | 低-中(需要列表同步机制) | Access Token(通过 jti) |
| SSF/CAEP 事件通知 | 秒级 | 低(事件驱动) | 高(双方需实现 SSF) | 全部令牌 |
生产环境推荐策略:
- Access Token 有效期 5-15 分钟 + Refresh Token Rotation(基础层)。
- 密码修改、账号禁用等关键事件:额外的吊销列表条目(在 Access Token 有效期内提供覆盖率)。
- 有条件的团队:实现 Introspection Endpoint 作为备选吊销路径(只在安全事件发生时使用,不作为日常验证路径)。
- SSF/CAEP 作为未来方向关注,等待标准成熟。
上一篇:JWT、JWS、JWE、JWKS 一次讲透 下一篇:MFA、TOTP、WebAuthn、Passkey 工程实践
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【身份与访问控制工程】IAM 全景:为什么这是高价值赛道
从 2020 年 SolarWinds 到 2024 年 Okta 支持系统泄露,身份基础设施的安全失败反复证明一件事:IAM 不是 IT 支撑系统,而是安全架构的承重墙。本文建立现代 IAM 的全景地图——从认证协议、令牌体系、权限模型到身份治理与平台选型,给出 5 个贯穿全系列的核心问题。
【身份与访问控制工程】OAuth 2.1 与 PKCE:现代授权主路径
OAuth 2.1 不是新协议,而是对 OAuth 2.0 的安全加固:废除 Implicit Grant 和 Resource Owner Password Grant,强制 PKCE 用于所有使用授权码模式的客户端,要求精确 redirect_uri 比对。本文从 PKCE 的密码学动机出发,拆解 OAuth 2.1 的授权码流程完整交互、Refresh Token 轮换与发送者约束、DPoP 令牌绑定,以及 DCR (Dynamic Client Registration) 和 RAR (Rich Authorization Requests) 的实际应用。
【身份与访问控制工程】JWT、JWS、JWE、JWKS 一次讲透
JWT 不等于 JWS。JWS 是签名格式,JWE 是加密格式,JWK 是密钥表示,JWKS 是密钥集合——这四个规范共同构成了 JOSE(JSON Object Signing and Encryption)技术族。本文从 JOSE 体系全景出发,逐层拆解 JWT 的三段式结构、JWS 的签名算法选择(从 HS256 到 EdDSA 的选择逻辑)、JWE 的密钥加密与内容加密双层模型、JWKS 的密钥轮换与缓存策略。
【零信任安全架构】身份感知代理:Google IAP、Pomerium 与零信任的入口
身份感知代理(IAP)是零信任架构中用户进入企业资源的唯一入口——它取代了 VPN 的'拨入内网'模型,把每个 HTTP 请求的认证和授权检查放在资源前面。本文拆解 IAP 协议层的完整请求流、JWT 验证的严格性要求、Header Injection 的安全陷阱,以及 Pomerium/oauth2-proxy/Cloudflare Access 的实现差异。