阅读提要
JWT 解决的是「跨服务信任」,不是「记住谁登录了」。把它当会话用,就会遇到「无法登出」「无法封禁」这两个死结。
一个常见的设计
登录成功后签发一个 7 天有效的 JWT,前端存 localStorage,每次请求带上。服务端验签通过就认为用户已登录。
这个设计能跑,但会遇到两个绕不过去的问题。
问题一:用户点了「退出登录」,但 token 还在有效期内。 前端删掉本地存储只是「不带了」,任何人拿这个 token 依然能访问。如果 token 泄露过,7 天内都有效。
问题二:管理员封禁了一个账号。 服务端没有任何地方可以「让这个 token 失效」——验签只检查签名和过期时间,不查库。封禁要等 7 天才生效。
这两个问题的根源相同:JWT 是无状态凭证,无状态就无法撤销。

JWT 到底解决什么问题
它的设计目标是:服务 A 签发,服务 B 能独立验证,不需要每次回源问 A。
典型场景是微服务之间、或者第三方授权(OAuth 的 access token)。这些场景的共同点是:验证方和签发方不在同一个进程里,且不共享数据库。
而「用户登录状态」的场景恰好相反:签发方和验证方是同一个服务,共享同一个数据库。用无状态方案解决一个有状态的业务问题,是自己给自己找麻烦。
那为什么大家都在用
因为它在早期看起来很方便:不用建 session 表,不用查库,天然支持水平扩展。
但「不用查库」这个优势,在你有 Redis 之后就不成立了——查 Redis 比验签多几毫秒,换来可撤销能力。
而水平扩展的问题,用共享 session 存储(Redis)同样能解决。
正确做法:会话用不透明 token
// 签发:随机串,不是 JWT
const token = crypto.randomBytes(32).toString('base64url')
await redis.set(`session:${token}`, JSON.stringify({ userId, role }), 'EX', 60 * 60 * 24 * 7)
校验时查 Redis。看起来多了一次 I/O,但换来三件事:
- 可撤销:退出登录直接
del; - 可封禁:封号时删掉该用户所有 session;
- 可续期:滑动过期,活跃用户不会突然掉线。
而且 token 本身不含任何信息,泄露了也解析不出用户 ID。
如果一定要用 JWT
有一种折中:短有效期 JWT + 刷新令牌。
- access token 有效期 15 分钟,无状态,验签即可;
- refresh token 存在服务端(数据库或 Redis),有效期 30 天,可以撤销;
- access token 过期后,用 refresh token 换新的。
这样「封禁生效延迟」最长 15 分钟,多数业务可以接受。代价是实现复杂度上升,要处理刷新失败、并发刷新、刷新令牌轮换等问题。
注意:access token 越短,无状态的收益越小。15 分钟的窗口里,查一次 Redis 的成本几乎可以忽略——这又回到「为什么不直接用 session」。
存储位置:别放 localStorage
无论用哪种 token,都不要放 localStorage。
localStorage 可以被同源的任何 JS 读取。一次 XSS 就能把 token 偷走,而且攻击者可以长期使用。
正确位置是 httpOnly + Secure + SameSite=Lax 的 Cookie:
res.cookie('session', token, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 7 * 24 * 3600 * 1000,
path: '/'
})
httpOnly 让 JS 读不到,XSS 拿不走。SameSite=Lax 挡住大部分 CSRF,配合「写操作校验 Origin 头」基本够用。
代价是跨域场景麻烦(需要 SameSite=None; Secure + CORS 配置),但比起 token 被偷,这点麻烦值得。
一个具体的判断清单
设计登录态时问四个问题:
- 需要「立即退出登录」吗?需要 → 不能纯 JWT。
- 需要「封禁账号立即生效」吗?需要 → 不能纯 JWT。
- 有多个服务需要独立验证身份吗?有 → JWT 有价值。
- 有共享存储(Redis)吗?有 → 不透明 token 更简单。
四个问题答完,方案基本就定了。多数单体应用的正确答案是:不透明 token + Redis。
收尾
JWT 是一个好工具,但它解决的问题很具体。把它当成「现代的 session」,就会陷入「为什么退出登录不管用」这种反复出现的困惑。
一句话记住:能撤销的才叫会话,不能撤销的只是通行证。
一个中间方案:服务端会话 + 客户端凭证分离
如果既想要 JWT 的无状态校验,又想要会话的可撤销,可以拆成两层:
- 短期 access token(5 分钟)用于服务间调用,纯验签;
- 长期 refresh token 存数据库,只有换发时才查。
代价是前端要处理「token 过期 → 自动刷新 → 重放原请求」这套逻辑,而且要处理并发刷新(多个请求同时发现过期,只应刷新一次)。
实现复杂度不低。如果只有一个服务,直接上 Redis 会话更省事——这也是多数项目的实际选择。
你认为这篇文章怎么样?
还没有表情反馈

一起聊聊
一个想法、一点补充,或一句简单的问候。
评论与回复
友善交流,彼此尊重第一句话,留给你。
读完有什么感受?欢迎成为第一个分享的人。