上周 HN 上一个叫「Stop Using JWTs」的 gist 拿了 400 多赞和 250 多条评论,讨论热烈得像是后端版的「vim vs emacs」。
正好这段时间在重构一个项目的认证模块,借这个机会聊聊这个话题。
JWT 的问题不在技术,在用途
JWT 本身是一个签名 token 标准,设计目标是非常短期的、跨服务的身份凭证——比如 SSO 登录时拿着 token 换 session,或者 API 间的服务认证。Google 用 JWT 做 SSO 传输层,但用户的实际登录状态仍然存在 session 里。
问题出在大量开发者直接把 JWT 当 session 用——签发一个有效期 7 天甚至 30 天的 JWT,存到 localStorage 里,每次请求带上,后端无状态验证。
这个用法有三个硬伤:
1. 撤销问题
Session token 过期可以从服务端删除。JWT 不行。一旦签发了,在过期之前它永远有效。如果想「踢掉这个用户」「重置所有设备的登录状态」「发现安全事件后强制下线」,靠 JWT 做不到——除非你维护一个黑名单,而维护黑名单等于又回到了有状态方案,那不如直接用 session。
2. 签名算法漏洞史
JWT 标准的历史漏洞可以说是教科书级别:从 alg: none 攻击(攻击者把签名算法改成 none 就能伪造任意 token),到 RS256 和 HS256 的密钥混淆攻击(服务端用公钥签名,攻击者诱导它用公钥当 HS256 的密钥去验),再到 CVE 列表里一堆算法实现上的 bug。
2017 年 Paragonie 那篇著名的分析文章结论非常直白:JWT 规范本身不被安全专家信任。
3. 「无状态」是一厢情愿
真实的认证系统不可能完全无状态。密码重置需要 token、多设备登录需要同步、权限变更需要立即生效——这些全都有状态需求。以为用了 JWT 就免了数据库查询,但实际只是把状态管理的复杂度从后端移到了前端和中间件层。
Session 的成熟方案
几乎所有主流框架都有内置或官方推荐的 session 方案:
| 框架 | Session 方案 |
|---|---|
| Express | express-session + connect-session-knex |
| Django | 内置 session 框架 |
| Rails | session 哈希 + cookie_store |
| Spring Boot | spring-session + Redis |
| Flask | flask-session |
核心区别在于 session 的数据存在服务器端,客户端只存一个无法伪造的 session ID。要撤销,删数据库记录就行。要延长,改过期时间就行。要审计,查 session 表就行。
Express 配置示例
const session = require('express-session');
const KnexSessionStore = require('connect-session-knex')(session);app.use(session({store: new KnexSessionStore({ tablename: 'sessions' }),secret: process.env.SESSION_SECRET,resave: false,saveUninitialized: false,cookie: {httpOnly: true, // 防止 XSS 读取 cookiesecure: true, // 仅 HTTPSsameSite: 'lax', // 防御 CSRFmaxAge: 7 * 24 * 60 * 60 * 1000 // 7 天}
}));
不会比配 JWT 多几行代码——大多数场景下甚至更少。session-store 的选择也很多:Redis(最快)、PostgreSQL/MySQL(最简单)、内存(开发用)。
如果要 short-lived token
上面说的都是「别用 JWT 做用户 session」。那真的需要 short-lived token 怎么办?
PASETO(Platform-Agnostic SEcurity TOkens)是 JWT 的替代标准,从设计上修复了 JWT 的所有已知问题:
- 不存在
alg: none攻击——算法在版本号中声明,不可伪造 - 默认使用 XChaCha20-Poly1305(对称)或 Ed25519(非对称),不是 HS256/RS256
- payload 加密是内建的,不是可选项
- 比 JWT 更短(版本号只有 2 字节)
# PASETO v4.local(对称加密)
pip install paseto
paseto create --key $KEY "{\"sub\":\"$USER_ID\",\"exp\":\"$EXP\"}"
但即使是 PASETO,也不建议用于用户 session。它适合的场景是 API 间凭证、密码重置 token、邮箱验证——就是那些确实需要「不查数据库就能验」的场景。
JWT 什么时候可以用
short-lived token + 非 session 场景。比如:
- SSO 传输:OAuth2 / OIDC 的 id_token 用 JWT 传递身份信息,接收方验签后换成 session
- API 服务间认证:微服务之间用 JWT 做短期凭证(5 分钟有效期),配合 mTLS
- 一次性操作:密码重置链接里的 token、邮箱验证 token,用完即弃
在这些场景下,JWT 的「不依赖数据库」和「跨服务验签」确实有优势。但别忘了 PASETO——一个从设计上就修复了 JWT 已知漏洞的新标准——在这些场景下同样适用。
如果你正在启动一个新项目的认证模块,或者准备重构老的认证代码,最不容易出错的选择仍然是传统的 cookie session。它经受了互联网二十多年的考验,而 JWT 作为 session 替代品的用法,还没有通过同样的时间检验。
参考:sam sch - Stop Using JWTs (HN 426 pts) / joepie91 - Stop using JWT for sessions / Paragonie - JWT is a Bad Standard
