别用 JWT 管理用户会话

参考:Stop using JWTs!(samsch)


一句话结论:JWT 不适合用来维持用户登录状态。 它本来就不是为持久会话设计的;真正干这件事的工具,是服务端 Session + Cookie,而且这套方案已经用了几十年。

先把概念对齐。很多人把「Cookie vs JWT」拿来对比,这本身就不对——Cookie 是存储/传输机制,JWT 是带签名的令牌格式,两者不是对立关系。真正该比的是:

  • 无状态 JWT:会话数据直接编码进 Token 里
  • 有状态 JWT:Token 里只放一个 Session ID,数据仍在服务端
  • 传统 Session Cookie:一个(可选签名的)Session ID,数据同样在服务端

本文不是说 JWT 永远不该用,而是:别把它当会话机制。 那样做既不合适,还有安全隐患。

常见误区

「无状态更好扩展」——只有无状态 JWT 在技术上才略沾边,但绝大多数应用根本到不了那个规模。单机多进程用 Redis 存 Session,多机再挂一台 Redis,现有框架都支持得很好。真到了要换方案的时候,全员登出一次就能迁移,不值得为了「未来可能」提前背上 JWT 的坑。

「JWT 更简单、更灵活、更安全」——恰恰相反。框架自带的 Session 开箱即用;灵活度上 Session 本来就能存任意数据;安全上,好的 Session 实现同样会用签名 Cookie,「用了密码学」不等于更安全。把 JWT 塞进 Local Storage 反而更危险——任何能执行 JS 的 XSS 都能把 Token 读走,而 HttpOnly Cookie 天生防这个。

「JWT 自带过期,不用管 Session 清理」——服务端过期同样好做,而且能在过期时清理数据库里的 Session 数据;无状态 JWT 过期前服务端毫无办法。

「用 JWT 就不用 Cookie 同意弹窗」——错。法规管的是持久标识符,跟你怎么存无关。功能性登录态本来就不需要额外同意;追踪分析才需要,换 JWT 也逃不掉。

「放 Local Storage 能防 CSRF」——CSRF 要靠 CSRF Token 防,跟 Session 机制无关。JWT 放 Cookie 一样有 CSRF;放 Local Storage 则强依赖 JS,还引入 XSS 风险。正确的 CSRF 缓解只有一种:CSRF Token。

「移动端/禁 Cookie 用户更适合 JWT」——移动浏览器和 HTTP 客户端都支持 Cookie;禁 Cookie 的用户通常也会禁 Local Storage。后端对移动端和 Web 端可以同一套 Session,Cookie 对 HTTP 客户端来说就是普通请求头,每次带上即可。

真正的代价

体积大。 无状态 JWT 把用户信息全塞进 Token,很容易顶破 Cookie 4KB 上限,被迫改用 Local Storage——然后回到上面的安全问题。

无法单独作废。 Session 可以随时在服务端踢掉;无状态 JWT 在过期前永远有效。用户改密码、发现账号被盗、撤销管理员权限——你都没法立刻让旧 Token 失效,只能等它自然过期,或者自己搭一套「黑名单」基础设施,那「无状态」也就名存实亡了。

数据会过期。 Token 里的角色、权限、资料都是签发那一刻的快照。用户刚被降权,手里还攥着写着 admin 的旧 Token,而你又作废不了它。

实现欠打磨。 有状态 JWT 本质上就是 Session Cookie 的劣化版——功能一样,却少了 express-session 这类久经沙场、反复修漏洞的实现。自己撸一套,很容易踩坑。

JWT 规范本身也饱受安全圈质疑:早期甚至允许 alg: none 伪造 Token,整体设计面向的是极短寿命(大约 5 分钟以内)的凭证,不是长登录态。

那 JWT 适合什么?

适合一次性、短时效的授权凭证,而不是持久会话。典型场景:

  • 文件下载:应用服务器验证登录后发一个几分钟有效的下载 Token,无状态的下载服务器验签后直接吐文件
  • 单点登录中转:Google 浏览器里用的是普通 Cookie Session;JWT 只在跨服务传递「刚登录成功」这一下,随后各产品各自建 Session
  • 微服务内部:网关验完 Session 后,用短寿命 Token 在内部服务间传递身份,避免每个服务都查一遍 Session 库——但即便这里,内网 API Key + 明文传用户 ID 往往也够,未必非 JWT 不可

好的 JWT 用法有几个共同点:短命、单次或极少次使用、应用本身仍用 Session 管登录。

该怎么做

直接用框架自带的 Session。对于 Javaer 来说 SpringBoot 封装的非常好,引入几个依赖写几行代码就搞定了,如果想要保存到 Redis 也是引入一个依赖的事情,非常方便!

总结

Cookie 是旧技术,但旧不等于差。安全领域,久经考验、没被打穿,往往比「新且未经充分检验」可靠得多。

Session 和 JWT 可以并存,各干各的——只是别把 JWT 当长期登录凭证。 用户登录这件事,交给 Session 就好。

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
leikooo
作者分享
提到用 Rust 重写项目,很多人的第一反应往往是性能提升。以 Bun 为例,它最初主要采用 Zig 开发,后来在 2026 年发布的 v1.4 版本中将核心代码迁移到了 Rust。用 Rust 重写,性能、安全性往往是大家想要的,但是有没有想过用 Rust 重写还能发现 bug 呢? pgrust 这个项就是,他用 Rust 重新实现 PostgreSQL,保持通信协议和 SQL 方言兼容。为了确认新系统的行为和原版一致,开发者需要对着 PostgreSQL 的实际返回逐个比对。结果查着查着,有些异常不是新代码的锅,PostgreSQL 自己就是这样的。pgrust 开发者 Michael Malis 把其中几个报告给了 PostgreSQL 社区。 大概的问题: 1)SP-GiST 空间索引里有个函数判断点的方位,代码认为上、下、同一水平线三种情况已经穷尽了,末尾放了句「impossible case」兜底。结果在特定浮点边界附近,三种判断同时不成立,程序真走进了这个「不可能」的分支 2)日期格式缓存复用时,一个解析模式的标记没重置,导致同一条 JSONPath 查询在同一个会话里,换个执行顺序结果就不一样。 3)pgcrypto 的 crypt() 在某些输入下截断了多字节字符,返回的文本编码不合法。能存进去,但 pg_dump 导出时就炸了。 一个用 Rust 重写的新项目,测着测着,把运行了几十年的 PostgreSQL 里埋着的东西翻了出来。Rust 重写的价值,不一定只在跑得更快、更安全这一件事上。为了做到真正的兼容,开发者得把前人没怎么碰过的边界全走一遍,这个过程本身就在帮原版排雷。
4
ARTS 1025: 唯一字符与同构字符串、省词可能增加阅读负担与重新估算软件的实现成本
5
🎉 1024 程序员节快乐! 祝编程导航里的每一位程序员:offer 多多,薪资翻倍,面试顺利,早日拿下心仪 offer!💻
17
AST:从 Java 代码理解抽象语法树
5
原来维基百科还有专门研究「AI 味」的页面 今天偶然发现一个挺有意思的维基百科页面:https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing,专门整理 AI 写作的典型特征。 里面总结了很多我们平时经常看到的 AI 味,比如喜欢用“值得注意的是”“在当今快速发展的时代”“综上所述”,还有过度使用排比、加粗、列表,以及一些看起来很有道理,实际上没什么信息量的表达。 感觉以后可以定期翻翻这个 wiki,尤其是经常使用 AI 辅助写作的人,应该能从中发现不少所谓的 AI 味。 链接: https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing
5
下载 APP