TT Lab
开始
学习 学习路径 课程

会话与令牌 — 从浏览器到服务网格

已经登出,令牌却还能用

在 TT Lab 中继续学习

一句话总结

签名的 JWT 访问令牌是资源服务器自己验证的。所以即使签发方说“这个令牌现在无效了”,API 也听不到,令牌会一直活到 exp。让撤销立即生效的方法只有三种——每次都向签发方询问(introspection)、让资源服务器持有黑名单、缩短令牌寿命以缩小暴露窗口本身。三种都不是免费的。

为什么需要它

用户点了退出登录,丢了笔记本电脑,密码泄露后改了密码。这时期待的是“从那一刻起,用那个人的令牌什么都做不了”。如果是上一个模块的服务器会话,删掉一个存储键就结束了。但 JWT 是自包含的。API 服务器只用 JWKS 检查签名和 exp 就放行,所以即使在签发方一侧删掉会话,它也无从得知。

RFC 7009 把客户端告知签发方“这个令牌我不再用了”的撤销端点标准化了。但这份 RFC 的第 3 节(Implementation Note)自己写明了局限:令牌如果是自包含的,立即撤销就需要签发方与资源服务器之间“currently non-standardized”的联动,另一种替代方案是用 refresh 令牌不断刷新寿命短的访问令牌。也就是说,调用了撤销端点,API 并不会自动知道。

工作原理

把三种策略并排放在一起是这样的。

전략                 폐기가 반영되는 시점        대가
짧은 TTL             최대 토큰 수명만큼 늦게     refresh 가 잦아져 발급자 부하
introspection        즉시                        요청마다 발급자 왕복, 가용성이 묶임
로컬 차단목록(jti)   즉시(목록을 가진 서버만)    상태 저장소, 서버 간 전파

撤销(RFC 7009)。客户端向 POST /revoke 发送 token 和可选的 token_type_hint(access_token、refresh_token)。机密客户端同时发送自己的认证信息,公共客户端同时发送 client_id,服务器会确认该令牌是否签发给了发出请求的客户端。无论撤销成功,还是令牌本来就无效,响应都是 200——因为客户端无法对这个错误做任何事。撤销 refresh 令牌时,如果服务器支持撤销访问令牌,就应该(SHOULD)把同一 grant 的访问令牌也作废,而撤销访问令牌时是否连 refresh 一起撤销,则是可选的(MAY)。

这个差别在实际工作中原样体现。在本实验的 Keycloak 26.0.7 上重新测试,撤销 refresh 令牌会使会话结束,该会话的访问令牌在 introspection 中变成 active=false,refresh 授权返回 400。而只撤销访问令牌的话,用 refresh 仍能不断得到新的访问令牌。这就是退出登录必须撤销 refresh 的原因。端点路径见 Keycloak 文档。

Introspection(RFC 7662)。资源服务器把收到的令牌以 form 形式 POST 到 RFC 7662 端点,就会收到 {"active": true, ...}。过期、已撤销、伪造时是 active=false,建议不要告知原因(SHOULD NOT)。这个端点为了防止令牌扫描,MUST 要求认证调用方——所以资源服务器以机密客户端(api-svc)的身份调用。RFC 7662 的安全注意事项指出了缓存的两面:缓存设短,数据最新,但签发方的负载增加;缓存设长,就会出现“已撤销的令牌仍可被使用的窗口”。

黑名单。把被撤销令牌的 jti 放进 Redis,每次请求都检查。关键是把 TTL 设为令牌的剩余寿命(exp - now)。过期的令牌在签名验证阶段本来就会被拒绝,所以之后没必要再记住,这样列表的大小就被限定为“当前仍然有效的已撤销令牌数”。

在现场相遇的样子

“退出登录了,API 却还能用”这类报告,通常是只做本地验证的 API 再叠加上较长的访问令牌寿命。这个 Realm 的访问令牌寿命是 900 秒,所以只做本地验证的话,已撤销的令牌最多还能活 15 分钟。缩短寿命,窗口会变小,但 refresh 流量会涌向签发方。RFC 9700(OAuth 2.0 Security BCP)第 4.14 节总结说,正因为有 refresh 令牌,才能签发短寿命的访问令牌来减小泄露的影响,并写明在密码变更或授权服务器退出登录等安全事件时,可以自动撤销 refresh 令牌(MAY)。

所以常见的组合是,以“短寿命访问令牌 + 退出登录时撤销 refresh”为基础,只在转账这类一次滥用也代价高昂的 API 上,再叠加 introspection 或黑名单。如果对所有 API 都用 introspection,签发方一变慢,所有服务就会一起变慢——验证成本变成了可用性耦合,就是这个节点。

下一项实验要做什么

启动 Keycloak,拿到令牌,用 introspection 确认 active=true,再按 RFC 7009 撤销 refresh 令牌,看它变成 false。接着在资源服务器上并排做出本地验证路径和 introspection 路径,确认已撤销的令牌在一边是 200、另一边是 401,并测量两条路径的延迟。最后加上 jti 黑名单和退出登录,把无需往返就能立即拦截的方法也在同一个流程中证明。