JWT vs Session:主流网站核心登录为何不用JWT,深入剖析原理、硬伤与落地场景
这几年关于 JWT 的文章快被写烂了,几乎每隔几天就能刷到一篇《为什么你应该用 JWT 替代 Session》之类的爆款。评论区永远是两拨人在吵架:一拨说 Session 早该进博物馆了,另一拨说 JWT 就是个华而不实的坑。我在团队里做过不少认证模块的选型和落地,也实打实踩过 JWT 续签、防篡改、密钥管理这些坑,今天想抛开情绪聊一个很多文章不敢说透的问题:既然 JWT 被吹得这么高,为什么你在微信、淘宝、支付宝、抖音这些主流产品里,几乎感受不到它的存在?或者说,主流网站在核心会话链路上,为什么宁可被骂技术老土,也不直接全量上 JWT?
这篇文章适合正在做技术选型的后端开发、架构师,也适合被各种 JWT 教程绕晕的前端同学。我会把 JWT 的原理、硬伤、适用场景、续签方案、漏洞防护和 SPA 落地讲清楚。说白了,是要搞清楚一件更重要的事:一个技术被吹上天的时候,你怎么判断它到底适不适合你。
1. 先看清本质:JWT 到底是什么,它凭什么被吹上天
1.1 JWT 的物理结构:三段式,到底哪一段不能瞎看
JWT 全称是 JSON Web Token,本质上是一串自包含的令牌。你把一个 JWT 用点号拆开,会发现它永远是三段式结构:
第一段 Header 是元信息,通常长这样:{"alg":"HS256","typ":"JWT"},用来告诉服务端这个 Token 是用什么算法签的。第二段 Payload 是负载,里面装着一堆声明(Claim),比如 sub(用户 ID)、iss(签发方)、exp(过期时间)、iat(签发时间),还可以塞你自定义的业务字段,比如用户角色、昵称。第三段 Signature 是签名,用 Header 里声明的算法和密钥,对前两段内容计算出来的结果。
这里有个最基本的认知,我见过很多同事踩坑:JWT 的 Payload 只是 Base64Url 编码,不是加密。任何人拿到你的 Token,都可以轻松解码看到里面的内容。签名的作用是防止数据被篡改,而不是防止别人偷看。你如果把身份证号、手机号、家庭住址直接塞进 Payload,和把这些信息写在明信片上寄出去没有本质区别。加密是另一个动作,要用 JWE(JSON Web Encryption)方案,但绝大多数场景只用到了 JWS(也就是签名版 JWT),并没有加密。
1.2 无状态认证到底解决了什么问题
JWT 最核心的卖点,是"无状态"(Stateless)。传统 Session 方案里,用户登录成功后,服务端要存一份 Session 记录,可能是内存、数据库或者 Redis,然后把一个随机的 Session ID 通过 Cookie 发给浏览器。下次请求带着这个 ID 过来,服务端得去查一下有没有对应的 Session,才能确认用户身份。这就带来一个问题:如果你的服务部署了多台机器,用户第一次请求落到了 A 机器,第二次请求被负载均衡转发到了 B 机器,B 机器上没有这个 Session,用户就掉线了。解决这个问题的经典方案是引入 Redis 做 Session 共享,或者用粘性会话把用户请求固定在一台机器上,但粘性会话又带来了扩容和容灾的麻烦。
JWT 的思路完全反过来:我不在服务端保存任何会话状态。用户登录成功后,服务端把用户 ID、角色、过期时间等信息打包进一个 Token,用密钥签名后发给客户端。后续每次请求,客户端把 Token 带上,服务端只需要验证签名、检查过期时间,就能确认用户身份。不需要查库,不需要查 Redis,天然适合分布式和微服务架构。这也是微服务教程里 JWT 出场率极高的原因——在概念上,它把"会话存储"这个环节彻底干掉了。
1.3 它就是一把"数字签名的通行证"
用生活化的类比来说,Session 方案像是你每次进小区都要去保安亭登记,保安查一查花名册,确认你确实住这个小区,然后放行。花名册是小区自己维护的。JWT 方案则像是小区发给你一张带防伪水印的出入证,上面写着你的姓名和楼栋号。保安不查花名册,只验证出入证的防伪水印是否有效,有效就放行。防伪水印就是 JWT 的签名,出入证上的信息就是 Payload。
这个模式在某些场景下确实香。典型场景是开放 API 和第三方授权。你给合作方发一个 JWT,合作方不需要连接你的用户中心数据库,只需要有你签名的公钥,就能自己验证 Token 是否合法。这种"自校验"能力,是 JWT 在开放生态里不可替代的价值。再比如网关鉴权,API 网关拿到请求里的 JWT,验签通过后直接把用户身份透传给下游服务,下游服务连解析都不用,省掉了一次次调用认证中心的开销。
2. 大厂不用 JWT 做核心会话,卡在哪些硬问题上
2.1 登录态说收就收:服务端主动注销的困境
JWT 最大的硬伤,是服务端无法主动让一个 Token 失效。注意是"无法主动"。Session 方案里,管理员封号只需要删掉对应的 Session 记录,用户下一秒请求就会因为找不到 Session 而被拒绝。JWT 方案里,用户手里的 Token 自带合法签名,你要让它失效,理论上只有一个办法:等它自己过期。
你可能说,那我把 Token 的过期时间设短一点,比如 15 分钟不就行了?问题来了:你设得再短,这 15 分钟内这个用户就是合法登录状态。如果用户因为盗号、违规内容被封禁,你 15 分钟才能生效,对安全团队来说这完全不可接受。更别说有些电商大促场景,运营要立刻把羊毛党踢下去,晚一分钟都是真金白银的损失。业界最通行的补偿方案是维护一个黑名单:把需要提前注销的 Token 的 jti(JWT 唯一标识)存到 Redis 里,每次请求先查一下黑名单。但你要真这么做,服务端又变成有状态了,而且每次请求还多了一次 Redis 查询,JWT 省下来的那点优势,被自己亲手还了回去。
2.2 改了密码,老 Token 还活着:状态同步的尴尬
第二个问题,是 Token 里装着的历史信息,和用户当前的真实状态会脱节。举一个真实场景:用户修改密码。Session 方案里,你删除旧的 Session 就完事了,所有老会话立即失效。但 JWT 方案里,用户改完密码,手里那个旧的 JWT 依然能被验签通过——因为签名没变、没过期。如果 Token 里还存着 role: admin 之类的角色信息,用户被降级之后,旧的 JWT 依然拥有管理员权限,直到 Token 过期。
有团队做了一种变通:数据库里存一个 token_version 字段,签发 Token 的时候把这个版本号写进 Payload,每次请求都查一次库对比版本号。版本号一变,所有老 Token 全部失效。但这样一来,JWT 无状态还剩多少?查询和比对数据库的操作,本质上已经让服务端重新有状态了。为了用 JWT 而强行把有状态的操作引进来,不仅没简化架构,反而比直接用 Session 更复杂——你既要维护会话,又要在不必要的地方引入无状态的包袱。
2.3 密钥管理:越安全的方案越不省心
JWT 的签名安全完全依赖密钥。签名算法主要有两类:HMAC 对称算法(HS256)和 RSA/EC 非对称算法(RS256/ES256)。对称算法的意思是签发和验证用的是同一个密钥,服务端得把密钥保护好。非对称算法是私钥签发、公钥验证,验证方只需要持有公钥。
问题在于,主流大型网站往往不是一个服务,而是几十上百个服务组成的微服务集群。如果采用 HS256,所有需要验证 JWT 的服务都得持有同一个密钥,密钥的存储、分发、轮换都是巨大的运维负担。哪怕有一个服务因为日志泄露把密钥暴露了,攻击者就可以伪造任意用户的 Token,整个认证体系瞬间崩溃。采用 RS256 会好一些,各服务只持有公钥,私钥集中在认证服务,但公钥的分发、轮换、版本管理同样需要一套基础设施。很多团队最后发现,搞 JWT 的密钥管理体系,比直接搞 Session 共享还累。
2.4 请求里的"大肚子":体积膨胀带来的连锁反应
JWT 的体积问题,很多人容易忽略。一个 Session ID 通常 20 到 30 个字节,一个 JWT 动辄一两 KB。为什么?因为它把用户信息都塞进 Token 里了。而 Cookie、Authorization Header、URL 参数,任何一个通道都在为这个体积买单。HTTP Header 在 nginx 默认配置下只有 4KB,你往 JWT 里塞的东西一多,再加上某些浏览器对 Header 体积的限制,生产环境很容易踩到暗坑。
更大的问题是移动端弱网环境。每次请求都背着一个 1KB 的 Token,一个月下来就是几百 MB 的重复流量,对数据流量敏感的用户来说是实实在在的体验损耗,对后端来说也是莫名的带宽压力和日志磁盘开销。再加上很多团队习惯在日志里打印请求头和请求体,JWT 里的明文 Payload 又成了用户隐私泄露的高风险区。我见过不止一次,线上日志平台里躺着完整可用的 JWT 副本,攻击者拿到日志就等于拿到了所有用户的登录凭证。
3. 主流网站的会话真相:Session、Cookie 和分布式
3.1 Session-Cookie 不是落后,是被大规模验证过的工程选择
聊到这里就得说句公道话:Session-Cookie 方案被主流网站沿用这么多年,不是工程师们懒,而是它在工程上确实够用、够稳、够可控。很多人以为 Session 方案在分布式环境下没法用,这其实是对 Session 的误解。Session 不是只能存在单机内存里,它可以被抽离成中心化的会话服务,存到 Redis 或专门的内存数据库中。每个服务拿到用户请求后,把 Session ID 交给会话服务去查,查到了就说明用户合法。这个方案虽然每次都要走一次网络请求,但换来的是服务端对会话的完全掌控:你随时可以删除会话、强制下线、查看用户所有登录设备。
现在主流的微服务架构里,会话状态早就不是"不能共享"的死结。Redis 集群、分布式缓存、会话粘滞、客户端 Cookie 加密……这些技术组合起来,支撑了亿级流量的电商大促和高并发秒杀场景。事实证明,在大规模工程场景里,可控性和可运维性比花哨的架构理念更重要。
3.2 主流网站的混合路线:JWT 也有它的位置
但你要是以为主流网站完全不碰 JWT,那也偏颇了。事实是,JWT 在主流网站的架构体系中,被用在了一些非常精准的位置上。最常见的例子是开放平台的 API 鉴权。你在开发者后台申请一个 AppKey,平台签发一个 JWT 作为访问令牌,第三方服务拿这个 Token 调你的 API,你的 API 网关验签后就能放行,不需要为每个第三方服务维护会话状态。
另一个典型场景是 OAuth2 授权码模式里的 Access Token。用户在第三方网站点击"使用微信登录",微信授权服务器确认用户身份后,会签发一个 JWT 作为授权令牌,第三方应用拿它去换取用户基本信息。这个场景里 JWT 是作为"一次性或短期的授权凭证"使用,过期快、无状态,配合微信自身的安全风控体系,非常有价值。也就是说,JWT 在网关认证、微服务间信任传递、授权码流程、短时效一次性令牌这些场景里,是主流选择。主流网站反对的不是 JWT 本身,而是把 JWT 当作全站登录态的唯一载体,这就是核心区别。
3.3 一张表看清三种方案的适用场景
| 对比维度 | 传统 Session-Cookie | 纯 JWT 无状态 | 混合方案 |
|---|---|---|---|
| 服务端状态存储 | 需要(内存/Redis/DB) | 不需要 | 视场景而定 |
| 主动注销/踢人 | 直接删除会话,立即生效 | 很难,需黑名单辅助 | 核心会话用 Session,边缘场景用 JWT |
| 分布式/微服务支持 | 需要中心化会话存储,方案成熟 | 天然支持,验签即可 | 各自取长补短 |
| 用户信息实时性 | 实时读取,天然一致 | 有延迟,Token 不更新就旧 | 核心信息走会话,非敏感信息走 JWT |
| 请求体积 | 小(Session ID 短) | 大(携带完整 Claims) | 视 Token 内容控制 |
| 安全风险 | CSRF 需防护,总体可控 | 密钥泄露、算法混淆,风险面大 | 需精细设计边界 |
| 典型落地场景 | 电商、社交、内容平台的核心登录 | 开放 API、微服务间鉴权、一次性凭证 | 大型互联网公司的主流做法 |
4. 如果非要用 JWT,怎么用才不踩坑
4.1 Token 续签的三种实现,各自适合什么团队
既然 JWT 有过期时间,那用户用着用着 Token 过期了怎么办?总不能让用户频繁重新登录。业界最常见的做法是双 Token 机制:一个短期 Access Token(比如 15 分钟到 2 小时),一个长期 Refresh Token(比如 7 天到 30 天)。Access Token 用来正常访问业务接口,过期后前端拿 Refresh Token 去换取新的 Access Token。Refresh Token 的作用只有一个——换新,因此即使被泄露,暴露窗口也能被控制在合理的时限内。
第二种思路是滑动过期。用户每次请求时,如果 Token 剩余有效期低于某个阈值(比如不足总时长的 30%),就由后端签发一个新的 Token,相当于每次活跃都在续期。这个方案用户体验好,缺点是每次都得多一次签发开销,而且你要设计好"免刷风暴"策略,避免大量请求同时触发续签。
第三种是最简单的"每次响应都重签"。服务端处理完请求后,如果不频繁,就直接顺手签一个新的 Token 塞到响应头里,前端下一次请求自动携带。这个方案实现成本最低,但会频繁触发令牌签发,对性能有隐性消耗,适合中小型内部系统,不适合高并发场景。选择哪种续签策略,核心要看你团队的运维能力和业务对"掉线容忍度"的预期。提醒一点:Refresh Token 一定要设置过期时间,而且不建议在移动端和 Web 端共用同一个 Refresh Token,否则一处泄露,全网遭殃。
4.2 防止数据被篡改:签名算法的正确选型
先说结论:在开放系统和多服务场景下,优先用 RS256 或 ES256,而不是 HS256。HS256 是同一个密钥既签名又验签,签名密钥一旦泄露,攻击者可以自己签发"合法" Token。RS256 是私钥签名、公钥验签,服务端持有的公钥即使泄露,也无法伪造 Token,安全边界清晰得多。密钥长度方面,HS256 的密钥建议至少 32 字节(256 位)的随机字符串,别用 "secret"、"admin123" 这种弱口令,网上有大量针对 HS256 的弱密钥字典爆破工具,几秒钟就能跑完常见口令。
除了选择算法,你还要注意签名验证环节的几个细节。第一,必须校验 alg 头,防止算法混乱攻击——攻击者把 alg 改成 none,或者把 RS256 改成 HS256,如果服务端不校验算法类型,直接用公钥当作 HMAC 密钥去验签,就可能被绕过。第二,必须校验 exp 字段,很多实现只验签不校验过期时间,等于是给攻击者留了个永不过期的后门。第三,必须校验 iss 和 aud 字段,防止一个服务签发的 Token 被拿到另一个服务去用,形成跨域越权。
4.3 JWT 漏洞清单:从 alg=none 到算法混淆
我把这些年公开和实战里遇到过的 JWT 漏洞整理成了一张清单,你可以在做安全评审时对照自查:
| 漏洞类型 | 原理说明 | 防护思路 |
|---|---|---|
alg: none 攻击 |
攻击者将签名算法改为 none,服务端若未正确校验,会接受无签名 Token | 显式拒绝 none 算法,严格校验 alg 头 |
| 算法混淆攻击 | 将 RS256 改为 HS256,诱导服务端使用公钥作为 HMAC 密钥验签 | 服务端固定算法白名单,不信任请求里的 alg |
| 弱密钥爆破 | HS256 使用了弱密码作为签名密钥,攻击者可通过字典攻击还原密钥并伪造 Token | 使用 256 位以上随机密钥,定期轮换 |
| 敏感信息泄露 | Payload 是 Base64Url 编码非加密,手机号、身份证等信息被直接读取 | Payload 只放用户 ID、角色等非敏感字段,敏感信息走服务端查询 |
exp 缺失或过长 |
签发 Token 时未设置或设置了极长的过期时间,导致 Token 长期有效 | 强制设置 exp,并基于业务场景控制有效期 |
| 密钥硬编码 | 签名密钥直接写在代码仓库或配置中心明文存储 | 使用密钥管理系统(如 KMS)托管,加密存储、定期轮换 |
| 日志采集泄露 | 请求日志打印 Authorization Header,JWT 被写入日志平台 | 日志脱敏,禁止打印认证头;Payload 不放置敏感信息 |
| 跨服务 Token 滥用 | 服务 A 的 Token 能访问服务 B 的接口 | 校验 aud(受众)字段,为不同服务签发不同 Token |
这些漏洞集中在两个根源:一是实现层面对算法和参数的校验不到位,二是对 JWT"明文可见、签名难管"的特性认识不深。记住一句话:JWT 的安全强度,取决于你对它的敬畏程度。
5. SPA 项目里的 JWT 落地:验证码、存储和刷新实战
5.1 Token 存哪里:localStorage 和 Cookie 的攻防取舍
前端单页应用(SPA)里用 JWT,最尴尬的问题就是 Token 该存哪。存 localStorage,好处是 JS 随手可取,配合 Authorization Header 用起来很自然,但只要是 XSS 漏洞,攻击者就能用一行脚本把 Token 偷走。存 Cookie,好处是浏览器会自动携带,XSS 拿不到,但代价是引入了 CSRF 风险,你需要额外配置 SameSite、CSRF Token 之类的防护。两边都有坑,安全界一直在争论,没有银弹。
我个人的取舍标准是这样:如果只是内部后台管理系统,信任度相对较高,可以用 localStorage + Authorization Header,注重开发效率。如果面向 C 端用户,安全等级要求高,优先考虑使用 Cookie 存储,SameSite=Lax + 自定义 CSRF Token 双保险,并且把刷新令牌放在 HttpOnly 的 Cookie 里。这样即使前端脚本被 XSS 打穿,攻击者拿不到 HttpOnly 里的刷新令牌,只能拿到短期 Access Token,影响范围被控制在很小的时间窗口里。
5.2 验证码流程配合 JWT:一次正常的登录链路
SPA 项目里经常遇到图形验证码或短信验证码登录,很多人不知道怎么和 JWT 结合。我分享一个比较成熟的流程:用户在登录页输入账号密码,提交前先请求一个 /captcha 接口,后端生成图形验证码(或下发短信验证码),并把验证码的正确值临时存到 Redis,同时下发一个 Captcha Ticket(可以用一个极短 TTL 的 JWT,比如 5 分钟),里面只包含这个验证码请求的标识 requestId,不包含任何用户敏感信息。前端提交登录表单时,把账号、密码、验证码以及这个 Captcha Ticket 一起提交。
后端收到登录请求后,先验证 Captcha Ticket 是否合法(验签 + 检查 exp),再对比用户提交的验证码和 Redis 里存的验证码是否一致。验证码一旦使用,立即删除 Redis 中的记录,防止重放。验证通过后,才正式签发 Access Token 和 Refresh Token。这个设计的精髓在于,验证码的核验被隔离成一个独立的短期权限,不需要把"用户输对了验证码"这个状态塞进业务 JWT 中,避免 JWT 承载过多一次性校验逻辑。短信验证码的限频、防刷,也可以用类似思路在签发 Captcha Ticket 之前做拦截。
5.3 Refresh Token 的完整流程和前端拦截
最后是 SPA 里双 Token 刷新的完整闭环。前端封装 axios 时,统一在请求拦截器里加上 Authorization: Bearer <accessToken>,在响应拦截器里对 401 状态做统一处理。触发 401 时,前端先判断是不是刷新请求本身失败,如果是就跳登录页;如果不是,就调用刷新接口,携带 Refresh Token 换取新的 Access Token,更新本地存储后重放原请求。
这里有几个特别容易踩的坑。第一个是并发刷新问题:Access Token 过期后,页面可能同时发出 5 个接口请求,全部返回 401,如果每个请求都去调刷新接口,等于同时刷新了 5 次,白白浪费资源,还可能因为刷新接口被限流导致全部失败。解决思路是维护一个"刷新中"的 Promise,多个请求共享同一次刷新。第二个是刷新失败后的会话清理,要记得清空本地 Token 和用户状态,跳回登录页,避免用户卡在一个"半登录"状态里。第三个是刷新接口本身要防重放,Refresh Token 建议设置了过期时间之后,最好加一层 jti 的 Redis 记录,使用一次就失效或标记,这样即使被截获,攻击者也无法长期复用。
最后的体会:JWT 不是不好,是"被用错了地方"
写到这里,我掏点个人感触。我在做技术选型时,越来越觉得判断一个技术"好不好",不能只看它的宣传亮点,更要看它在你的业务场景里是否成立。JWT 的无状态特性,在微服务、开放 API、分布式信任传递里确确实实有不可替代的价值,但它从来不是"替代 Session"的银弹。主流网站不把它当作核心登录态,不是因为大厂不接受新技术,而是因为在真实业务里,"服务端能主动控制会话"这件事,比"无状态"的值钱得多。Session 方案经历了十几年的高并发考验,它的可掌控性、可审计性、一致性,才是大型系统最稀缺的品质。
我对 JWT 的态度是:用,但用在对的地方。对外提供 API 鉴权,用 JWT;微服务之间传递身份,用 JWT;短时效的一次性凭证,用 JWT。但是用户登录态、权限管理、封禁踢人这些核心链路,如果服务端需要强大的控制力,那就老老实实回到 Session 或者"Session + JWT 混合"的思路。这不是技术上的退步,是工程上的务实。至少在我经历过的项目里,认清了这一点之后,认证出问题的概率才真正降下来。