被吹上天的 JWT,为什么主流网站一个都不用

打开浏览器的开发者工具,翻一翻 github.com 的 Cookie。里面只有一条 _gh_session,一串 Rails 框架加密后的会话数据。再去 google.com 看,SID、HSID、SSID、SAPISID 排成一列,Google 的隐私说明写得明明白白:这些 Cookie 存的是经过数字签名和加密的账号 ID 与最近登录时间。

Amazon、Netflix 也是同样的配置。Stack Overflow 看起来特殊一点,用的是 ASP.NET 的 Forms Authentication,名字唬人,本质仍然是服务端 Session。

没有一条是 JWT。

这个事实在社区里被反复提起,又反复被遗忘。十年来,JWT 在教程、面试题和脚手架模板里出镜率极高,以至于很多后端开发者默认它就是现代认证的标准答案。但翻遍这些网站的真实登录态,会发现一个尴尬的情况:几乎所有主流 Web 网站的浏览器端认证,用的都是 Session 加 Cookie。

JWT 的主战场从来不在浏览器

先说清楚一件事:JWT 并非一无是处,它只是被放错了位置。

JWT 真正适合的场景是这些:服务之间的内部调用,自包含的身份信息省去重复查询,网关与服务网格生态对它的支持已经很成熟;需要离线校验的边缘节点,本地验签是不得已但合理的选择;开放平台里机器身份的自证,GitHub 就拿它干这件事。至于移动端 App 拿凭证调 API 该用什么格式,市场上有多种选择,大厂的答案也未必是 JWT,但无论选哪种,这个场景和浏览器会话都是两码事。

换句话说,JWT 是一份自带签名的凭证,适合在不受信任的边界之间传递身份。但它从头到尾都不是为浏览器会话设计的。Stack Overflow 上有一个被引用很多的高赞回答,说得非常直白:JWT 从来不是用来管理会话的,它是服务之间交换完整性保护消息的工具。

把这个工具硬套到"用户在浏览器里登录"这个场景上,问题就一个接一个地来了。

撤销不了,是硬伤

用户账号被盗,管理员要立刻把这个人踢下线。用 Session,服务端删掉那条会话记录,下一秒用户就掉线了。用 JWT,签出去的 token 在过期之前始终有效,服务端没有任何官方手段作废它。

社区里流传很广的一句吐槽来自 Reddit 的 node 版块:不喜欢 JWT,因为它难以控制------账号被黑之后,没有任何办法让那个 token 失效。想要失效,唯一的办法是维护一个黑名单------每个请求都要查一遍黑名单,token 才有意义。而一旦这么做,JWT 引以为傲的"无状态"就不存在了。

Hacker News 上一条高赞评论把这件事总结得更彻底:JWT 的目标是无状态,但只要需要可撤销,它就不可能无状态。

有人会退一步:缩短过期时间,比如五分钟,配合 refresh token 续期,把吊销控制在续期边界上。这个方案确实是现在一些认证服务商的推荐做法。但代价是引入了一整套双 token 机制,而且五分钟窗口内的攻击依然拦不住。安全厂商 WorkOS 在分析这个问题时提到,企业客户最常见的第一条需求就是"能不能立刻让某个用户下线",而无状态 JWT 恰好做不到这一点。

95% 的 JWT 使用,只是在模拟 Session

Reddit 的 PHP 帮助版块有个说法流传很广:绝大多数时候,人们用 JWT 只是在模仿 Session。

看看身边的项目就明白了。签发一个只装着 userId 的 token,每次请求收到后解析出来,然后去数据库查这个用户的详细信息,判断状态是否正常、角色有没有变。这一套下来,数据库查询一次没少,额外的开销是签名和验签。Session 里也存了 userId,也能查库,还能随时销毁。区别只剩下一个:JWT 的版本更重。

还有一层更隐蔽的问题。JWT 的签名只能保证 payload 没被篡改,不能保证 payload 里的信息仍然成立。一个 token 在签发那一刻写着 role: admin,两个小时后管理员权限被回收了,这个 token 里的 admin 依然畅通无阻,直到过期。Session 没有这个问题,因为权限判断永远以服务端的实时数据为准。

安全性并没有更好,反而更差

JWT 的 payload 只是 Base64 编码,不是加密。把 token 拆开贴到任何解码工具里,内容一目了然。放到浏览器场景里,这意味着所有用户信息对同源的 JavaScript 完全可见。

于是存储位置变成了一个两头堵的选择题。放在 localStorage,XSS 攻击直接把 token 偷走,安全社区的共识是这条路风险最高。放进 Cookie,XSS 的问题确实缓解了,但此时它和一条 Session Cookie 面临的攻击面完全一样------CSRF 要防,SameSite 要设。绕了一圈,JWT 没有带来任何额外保护,只把 payload 内容多暴露了一份。

JWT 自身的实现史也不太光彩。安全研究者曾经批量指出主流 JWT 库的算法混淆漏洞:攻击者把 header 里的算法改成 alg: none,去掉签名,服务器照样放行。知名认证服务商 Auth0 的 API 就中过这类招,攻击者可以伪造任意用户的 token。这类漏洞的根源在于 JWT 把太多决定权交给了实现者------验签逻辑稍有不严谨,整个认证体系就穿了。

Java 生态里更著名的例子是 Shiro 的 RememberMe 反序列化漏洞,编号 CVE-2016-4437,因为默认使用了 AES 加密且密钥硬编码在代码里,攻击者可以构造序列化数据实现远程代码执行。这个漏洞给整个行业提了个醒:凭证体系里任何"本地保存的密钥"都可能成为突破口,凭证机制越复杂,出错的面就越大。

顺带纠正一个流传很广的说法:Session 更容易被 CSRF 攻击。实际上 CSRF 防护和认证方式无关,靠的是 SameSite 属性和 CSRF Token。而 JWT 因为放在 Header 里天然免疫 CSRF,代价是必须存进 localStorage,把 XSS 风险请了回来。两种方案各有各的坑,Session 的坑更小,防护手段更成熟。

SPA 是 JWT 的正当主场吗

有一个场景需要认真对待:前后端分离的 SPA。Vue、React 的项目里,JWT 加 localStorage 的写法几乎是教程标配,看起来 JWT 在浏览器里找到了自己的正当主场。

把存储方案摊开看,SPA 面对的是一个三难困局。放 localStorage,任何一段被注入的脚本都能把 token 读走,XSS 一旦发生就是登录态裸奔。放内存,刷新页面就丢,只能靠静默刷新不断续签,复杂度不比双 Token 少。放 HttpOnly Cookie,脚本摸不到了,但 CORS 配置、Cookie 的 SameSite 策略都得跟着调------而这正是很多团队当初逃离 Cookie 的原因。

安全社区在这件事上论证得很透:CSRF 和 XSS 不是并列的风险,XSS 永远是更致命的那个,攻击者一旦能执行脚本,任何 CSRF 防御都形同虚设。所以存储的第一原则是让脚本读不到 token,HttpOnly 是底线,其他两条路都是在给底线打折。

规范层面已经给出了答案。IETF 在 2025 年把《OAuth 2.0 for Browser-Based Apps》正式定稿为 RFC 10017,推荐的架构叫 BFF,即 Backend for Frontend:SPA 只和一个轻量后端对话,access token 和 refresh token 全部留在服务端,浏览器拿到手的只有一条 HttpOnly 会话 Cookie。授权码加 PKCE 是固定动作,refresh token 的重放检测是 RFC 9700 里的明确要求。

看清楚这个走向了吗?绕了一圈,SPA 的规范答案还是 Session Cookie。那些坚持纯前端、不引入后端的团队,只能退一步用 localStorage 加 refresh token 轮换,认证服务商社区里确实有人推荐,也有安全研究者专门写文章批判这种折中的脆弱------它是安全下限,不是推荐架构。

所以 SPA 用 JWT 的真相是:它多半是前后端分离早期、Cookie 被 CORS 配置劝退之后的将就,而不是浏览器场景的新答案。BFF 一上,登录态回到 HttpOnly 会话 Cookie 里,JWT 在浏览器里连容身之处都没有。承认这个用法普遍存在,和承认它是无奈之选,是两件事。

双 Token 机制的复杂度被严重低估

JWT 的过期机制催生了 access token 加 refresh token 的双 Token 架构,然后复杂度就开始指数级增长。

refresh token 要存储、要轮换,防止重放攻击要做复用检测,过期边界要处理,前端请求队列要在 token 刷新期间挂起重发,多个标签页之间要同步登录态。中文社区里关于"无感刷新"的踩坑文章汗牛充栋:并发请求撞上 401 之后排队刷新、刷新接口的竞态条件、轮换后旧 token 失效引发的连环失败,每一个都是生产事故的常见来源。

Session 这边呢?设置一个 maxInactiveInterval,到期自动失效。活跃用户的会话在滑动窗口内一直续命,不活跃的用户安静地被清理。一行配置解决的事情,双 Token 架构要用一整套基础设施来模拟。

"分布式所以要用 JWT"是传播最广的误解

支持 JWT 的人有一句万能话术:分布式系统下 Session 没法共享,所以要用 JWT。

这个论点在十年前可能成立,如今早就不是了。Spring Session 加 Redis 的组合把分布式会话变成了一个引入依赖就能解决的问题,会话数据集中存储,所有节点共享,水平扩展毫无压力。这套方案在生产环境里被反复验证过,阿里云、腾讯云的开发者社区里大量实践文章可以做证。

反过来想,JWT 真的让系统变成无状态了吗?上一节说过,为了吊销能力,黑名单还是要建,黑名单还是要放在 Redis 里。绕了一大圈,该有的集中存储一样不少,还多出了签名验签和 payload 膨胀的成本。体积上,Session ID 只有几十个字节,JWT 动辄几百上千字节,每一个 API 请求都要背着这串东西跑。

无状态确实是一个好特性,但它解决的是"认证中心单点"的问题,而不是"认证"本身的问题。为了一个部署架构上的便利,把安全性和可控性打七折,这笔账怎么算都不划算。

token 加 Redis,是第三条路还是换皮 Session

中文社区还有一条流传很广的路线:不用 Session 容器,登录成功后生成一个随机字符串发给客户端,状态存在 Redis 里,网关每个请求查一次。sa-token 把这条路线做成了开箱即用的框架,默认模式就是不透明 token 加 Redis,功能列表里有踢人下线、账号封禁、分布式会话、微服务网关鉴权。注意它官方的自我定位,写的正是分布式 Session。

这条路线的语义,拿 Session 的定义对照一下:一个随机标识,客户端持有,服务端按标识找到状态。这就是 Session,一个字都不用改。Reddit 的 golang 版块有人专门提问"带 Redis 的 JWT 和 Session 有什么区别",回答的方向出奇一致:往认证体系里加进 Redis 校验的那一刻,它就已经不是什么新东西了,它是一个自己手写的 Session。

真实的差异只有三点。生成方不同,Session ID 由框架或容器生成,这条路线由自己的代码生成。存储位置不同,Session 在容器内存或框架管理的共享存储里,这条路线显式放在 Redis,天然跨节点共享。通道不同,Session 走 Cookie 自动携带,这条路线走 Header,对 App 和小程序更友好。除此之外,超时、续期、并发登录控制这些会话语义,两条路线一模一样。

所以这条路线值得选,但要认清选的是什么:它不是 Session 的替代品,是 Session 的手工版。买到的是 Header 通道的自由和状态管理的显式控制,付出的是自己实现全套会话语义、自己扛所有边界情况的代价。Redis 官方博客把 token 存储和会话存储并列成两个专题讨论,业界从来没人把这条路线当成超越 Session 的新范式。

真正的问题不是选谁,是状态放哪里

把 Session、token 加 Redis、JWT 三条路线摆在一起,会发现争论的焦点一直不太对。真正的问题是:登录状态放在哪里,认证结论允许陈旧多久。

放的位置有四种。应用进程内存,最传统,重启即失,天然单机。共享缓存,Spring Session、sa-token、OAuth2 授权服务器都属此类,业务节点无状态,缓存中心承担全部会话。客户端签名,JWT,状态冻结在 token 里,服务端不存。第四种最近几年才成型:边界兑换,网关对外的凭证不透明,进了信任边界再兑换成 JWT,外部可控、内部高效。

新鲜度是另一根轴。每请求查一次 Redis,认证结论零延迟,吊销即时生效;校验结果缓存几秒,用极短的吊销延迟换掉绝大多数查询;JWT 把新鲜度拉到极限,签发那一刻的快照在过期前始终成立。三条路线其实是同一根拨盘上的三个刻度,从来就不存在"无状态对有状态"的对立。

用这把尺子量一下现实,JWT 的无状态宣传就更站不住了。中文微服务社区的标准配置就是 JWT 加 Redis 黑名单,网关层判黑名单是标配动作,阿里云的微服务引擎甚至专门做了 jwt-logout 插件,官方描述里出现了一个精准的词:对 JWT 的弱状态管理。一个要靠"弱状态管理"插件才能支撑登出功能的方案,说它无状态,多少有点自欺欺人。

什么时候该用什么,一张表说清楚

场景 推荐 理由
传统多页 Web 应用 Session + Cookie 可撤销、简单、HttpOnly 防 XSS、SameSite 防 CSRF
SPA 单页应用 BFF + HttpOnly 会话 Cookie RFC 10017 推荐,token 不进浏览器
纯前端无后端折中 localStorage + 轮换 + 严格 CSP 安全下限方案,重放检测不可省
移动端 App OAuth2 不透明 Access Token 授权码加 PKCE,token 存 Keychain 或 Keystore
单体 API 多端共用 不透明 token + Redis Header 通道对 App 友好,本质是手工 Session
微服务内部调用 JWT 或 mTLS 短时效自包含凭证,限信任边界内使用
开放第三方 API OAuth2 Access Token 授权服务器统一管理,支持吊销与范围控制
SSO 单点登录 中心化认证服务 各端共享同一个后端,网页多一层 Cookie 包装

OWASP 的会话管理规范也给出了明确建议:登录成功后重新生成会话标识、设置软硬两种超时、Cookie 使用 __Host- 前缀并配齐 HttpOnly、Secure、SameSite 属性。这一整套要求全部围绕服务端 Session 展开,没有给 JWT 留位置。

浏览器端的认证选型,答案早就写在这些网站的 Cookie 里了。

回到工程判断

作者手头有个 Java 项目,早先也跟着潮流上了 JWT,双 Token、无感刷新、黑名单一样不落。后来维护的人受不了这套复杂度,评估之后回归了 Shiro 的 Session 管理,所有的踢人、续期、并发登录控制都回到了熟悉的服务端逻辑。当初项目的备注里留了一句话:既然 Shiro 有一套会话服务,又何须 token。

这句话和主流网站的选择,是同一个判断。

所谓无状态,在工程上从来是个刻度问题;所谓新范式,很多只是旧概念换了外壳。技术选型不需要追赶热度。JWT 在它该在的地方------服务间凭证、离线校验、机器身份------依然是称手的工具。但在浏览器会话这个场景里,几十年演进下来的 Session 加 Cookie,配合今天成熟的分布式方案,仍然是那个最省心、最可控、被验证最充分的答案。

主流网站用脚投票的结果,值得每一个做技术选型的人停下来看一眼。

相关推荐
金銀銅鐵8 小时前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(上)
java·后端·ai编程
念何架构之路9 小时前
go-grpc设计思想
开发语言·后端·golang
system_error_x9 小时前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
步行cgn11 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
码事漫谈12 小时前
AI 读你的提示词,其实很没耐心
后端
用户83562907805113 小时前
如何使用 Python 格式化 Excel 单元格中的长文本
后端·python
我命由我1234513 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
IT_陈寒13 小时前
Java性能优化完全指南
前端·人工智能·后端
电子IT笔记13 小时前
对接群晖NAS,上传和删除踩坑记录
前端·后端
Mk5987813 小时前
P4:把 Agent 的记忆搬进 Redis —— AgentScope-Java 工程化实战
后端