上周 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