失效这个难题
一句话总结
JWT 签发后无需服务器介入,因此速度很快。也正因如此,它很难被撤销。 理解这项取舍,并通过有效期管理它,才是实际工作中的做法。
为什么需要了解这些
“已经退出登录,令牌却仍然有效”是使用 JWT 的团队必然会遇到的问题。在基于会话的认证中,服务器从会话存储中删除记录即可。JWT 不在服务器保存状态,因此没有可删除的内容。
人们很容易想到黑名单方案:把已撤销的令牌 ID 放入存储,并在每次请求时查询。但这样每个请求都会产生一次存储查询,无状态的优势也随之消失,与重新采用会话方式没有太大区别。
通过有效期进行管理
实际工作的标准答案,是为两种令牌设置不同的有效期。
| 访问令牌 | 刷新令牌 | |
|---|---|---|
| 有效期 | 5~15 分钟 | 数天到数周 |
| 验证 | 只验证签名(无状态) | 认证服务器保存状态 |
| 立即撤销 | 不可能 | 可以 |
| 存放位置 | 内存 | httpOnly Cookie 或安全存储 |
访问令牌有效期较短,即使被盗,也会在这段时间后失去作用;撤回权限后,也会在这段时间内生效。刷新令牌的有效期较长,但必须进行轮换:每次刷新都签发新令牌,并让旧令牌失效。
这一组合的效果很重要。退出登录时撤销刷新令牌。访问令牌虽然仍然存在,但最多 15 分钟后就会过期,此后又无法刷新,因此会话实际上已经结束。无需黑名单,就能实现 15 分钟内失效。
轮换还有一项附带效果。如果旧刷新令牌再次被使用,这就是被盗信号,因为正常客户端持有的是新令牌。此时让该用户的整个令牌族全部失效,就是重用检测(reuse detection)。
정상: RT1 → (갱신) → RT2 → (갱신) → RT3
탈취: RT1 → (갱신) → RT2
└── 공격자가 RT1 을 다시 사용 → 서버가 계열 전체를 끊는다
只有真正需要立即中断的情况——例如账户被盗申报、管理员强制退出——才例外使用黑名单。目标只是该用户的令牌,而不是所有令牌,因此存储负担较小。更轻量的办法是为每个用户保存一个 token_not_before 时间,拒绝所有早于该时间签发的令牌。存储中每个用户只需一个值。
Keycloak 的四种有效期设置
Keycloak 中有多项有效期设置,容易混淆;它们分别控制不同内容。
| 设置 | 控制内容 | 常见值 |
|---|---|---|
| Access Token Lifespan | 访问令牌的有效时长 | 5~15 分钟 |
| SSO Session Idle | 无活动时多久结束会话 | 30 分钟 |
| SSO Session Max | 即使持续活动也强制结束的时间 | 10 小时 |
| Offline Session Idle | 离线令牌的空闲上限 | 30 天 |
空闲时间与最大时间之间的区别是关键。空闲表示“停止操作就会中断”,最大表示“即使一直使用,最终也会中断”。银行等受监管机构会把最大时间设得较短;内部工具则会延长最大时间,让用户每天只需登录一次。
如果两者关系设置错误,就会出现异常现象。Access Token Lifespan 长于 SSO Session Idle 时,会出现会话已经结束、访问令牌却仍然有效的时间窗口。访问令牌有效期必须始终短于空闲时间。
限制并发登录也是常见需求。Keycloak 提供会话数限制功能;如果要在应用层处理,则必须另外维护每个用户的会话列表。
后通道退出与 BFF
多个应用程序共用同一认证服务器时,在一个应用中退出登录,其他应用也应同时结束会话。OIDC 的后通道退出是由认证服务器直接向各应用程序登记的地址发送退出令牌。它不经过浏览器,因此即使标签页已经关闭也能工作。接收方验证该令牌的签名与 sid,然后删除对应会话。
BFF(Backend for Frontend)模式在会话管理方面也有优势。由后端保存令牌,只向浏览器提供会话 Cookie,退出时删除该会话即可立即使其失效,同时也消除了通过 XSS 窃取令牌的路径。但后端因此会保存状态,在水平扩展时需要会话存储。
令牌应该存放在哪里
存储位置与有效期设计同样重要。如果位置错误,无论有效期多短都无济于事。
| 位置 | 防 XSS | 防 CSRF | 刷新后保留 | 评价 |
|---|---|---|---|---|
localStorage |
✗ 脚本可以读取 | ✓ | ✓ | 最常见,也最危险 |
sessionStorage |
✗ | ✓ | 按标签页 | 并没有更好 |
| JavaScript 变量 | △ 同一上下文可读取 | ✓ | ✗ | 与刷新机制搭配时实用 |
httpOnly Cookie |
✓ 脚本无法读取 | ✗ 需要 SameSite | ✓ | 适合放刷新令牌 |
实际工作的组合如下:访问令牌放在内存中,刷新令牌放在启用 httpOnly + Secure + SameSite=Lax 的 Cookie 中。 刷新页面后,内存中的访问令牌会消失,但可以借助 Cookie 中的刷新令牌静默获取新令牌。
SameSite=Strict 会导致用户通过外部链接进入时不携带 Cookie,看起来像是退出了登录。多数情况下使用 Lax 更合适,只有支付等敏感流程才应缩紧为 Strict。
时钟偏差会造成什么问题
JWT 验证会根据服务器时钟检查 exp 与 nbf。认证服务器与资源服务器的时钟即使只相差几秒,也可能间歇性出现“刚收到的令牌尚未生效”(违反 nbf)的错误。这类问题很难复现,往往会耗费很长时间排查。
验证库通常允许 30~60 秒的余量(clock skew)。不要把它缩减为 0,而应为所有节点配置 NTP。
生产现场会看到什么
令牌有效期配置在界面上只是几个数字,但这些数字造成的症状,可能看起来与认证毫无关系。
“偶尔会退出登录”通常是会话空闲时间导致的。 即使访问令牌持续刷新,如果 SSO 会话的空闲时间过短,短暂离开的用户回来时,刷新请求就会被拒绝。用户会说“才过几分钟就掉线了”,而日志只会记录一次正常过期。
“已经退出,但其他标签页仍然有效”说明没有接入后通道退出。 只使用前通道,其他已经打开的客户端就无法得知退出事件。如果系统没有保存会话 ID,也没有接收退出通知并删除会话的位置,退出只会发生在当前浏览器标签页中。
“每次部署后所有人都会退出”说明签名密钥发生了变化。 轮换密钥时,应暂时保留旧密钥用于验证;同时还要检查缓存公钥列表(JWKS)的一方多久刷新一次。
“只有某些用户无法使用”通常是令牌变大了。 拥有大量组或角色的账户,其令牌会变大;如果把令牌放进 Cookie,就可能超过请求头大小上限,代理会返回 431 或 400。此时呈现的症状不是认证错误,而是所有页面都无法显示。
“时间总有一点偏差”确实就是时钟问题。 签发服务器与验证服务器的时钟即使只差几秒,nbf/exp 的判断也会不同。应同步 NTP,并在验证端保留几秒余量(clock skew)。
调查时不要把完整令牌写入日志。 令牌本身就是凭据。真正需要的信息只有 sub、sid、exp 等字段,因此只提取并记录这些内容。
下一次检查要看什么
本模块将通过测验检查有效期设计的判断标准,并结束本课程。