JWT原理与实战:为什么大厂不买账,以及如何正确设计Token认证方案
JWT这几年在技术圈被捧得挺高,张口闭口无状态认证、分布式友好、跨域神器,感觉不用 JWT 就落伍了。但你真去翻主流网站的实现,会发现一个挺有意思的现象:绝大多数体量靠前的互联网产品,压根没把 JWT 当核心登录态来用。我这些年经手过不少认证系统的改造,也用 JWT 踩过坑、填过坑,今天想把这个问题聊透:JWT 到底行不行,为什么大厂不买账,以及如果你非要用,该怎么用才不至于翻车。
这篇文章适合谁看?正在做登录认证选型的后端开发、打算自研网关鉴权的架构师、还有那些被产品经理一句“别人都用 JWT”逼着改方案的兄弟。我会把 JWT 的原理、痛点、替代方案以及实操中的续签、防篡改、漏洞排查一次讲清楚。
1. 内容整体设计与思路拆解
1.1 JWT 为什么会被捧上天
JWT 全称是 JSON Web Token,本质是一段自包含的令牌,由 Header、Payload、Signature 三部分组成,用点号分隔。Header 里存签名算法,Payload 里塞用户 ID、过期时间这些业务声明,Signature 则是对前两部分的签名。因为令牌自包含,服务端不需要存 session,拿到 token 验个签名就能确认身份。
这个特性在理论层面确实漂亮,尤其是在微服务架构下,每个服务都能独立验签,不需要每次都去 session 服务里查状态。加上 JSON 格式天然跨语言,移动端、小程序、第三方开放平台对接起来确实省事。我最早接触 JWT 也是因为要做一个开放 API 平台,手头没有现成的会话系统,JWT 几乎是开箱即用。
还有一个原因让 JWT 在技术圈传播得特别快:写起来太简单了。几乎所有语言都有现成的库,几行代码就能签发和验证,比维护一套 session 存储省心太多。年轻团队做技术选型时,很容易被这种“低成本、高逼格”的方案吸引。
1.2 主流网站的真实选择是什么
如果你去看主流网站的登录实现,绝大多数走的是传统方案:用户登录后,服务端生成一个不透明的 session ID,把这个 ID 存在 Redis 里并设置过期时间,同时下发一个 httpOnly 的 Cookie 给浏览器。用户每次请求带上 Cookie,服务端拿着 session ID 去 Redis 查用户状态。
这个方案看起来“土”,但稳定性极高。会话在服务端可控,想踢人直接删 Redis key,想续期直接改 TTL,用户改密码后可以把所有旧会话全部失效,这些操作对 JWT 来说都是噩梦。主打无状态认证的 JWT 反而成了负担。
这不是说主流网站技术老旧,恰恰相反,越是体量大的系统越倾向于可控性强的方案。登录态属于核心资产,一旦出了问题就是安全事故加用户投诉双重爆炸,稳妥压倒一切。
1.3 两种方案的适用边界
会话方案适合绝大多数传统 Web 应用、后台管理系统、需要强制下线能力的业务系统。JWT 则适合开放 API 鉴权、临时授权、服务间通信、移动端无状态接口这些场景。两者不是替代关系,而是互补关系。
我见过不少团队一上来就全站 JWT,结果做到后面发现需要服务端踢人、需要会话续期、需要多端登录管理,只能硬着头皮在 JWT 之上再加一层 Redis 黑名单,等于绕了一圈又回到了状态化方案,还白白背上了 JWT 体积大、解析开销高的包袱。
2. 核心细节解析与实操要点
2.1 JWT 的三段式结构和签名原理
先看一个典型的 JWT 长什么样:
第一段 Header,Base64Url 编码,内容一般是 {"alg":"HS256","typ":"JWT"},指明签名算法。第二段 Payload,存业务数据,标准字段有 iss(签发者)、exp(过期时间)、sub(主题)、aud(受众)等,也可以自定义字段。第三段 Signature,是对前两段加上密钥做签名计算的结果。
以最常用的 HS256 为例,签名过程是:HMACSHA256(base64Url(header) + "." + base64Url(payload), secret)。验签时服务端用同样的密钥重新计算签名,比对是否一致,一致就认为数据没有被篡改。
注意:JWT 的 Payload 只是 Base64Url 编码,不是加密。任何拿到 token 的人都能解码看到里面的内容,所以敏感信息绝对不能放进 Payload,比如手机号、身份证号、密码哈希之类的一律不能放。
2.2 为什么说 JWT “被吹上天”
JWT 最大的卖点是“无状态”,但这个卖点在实际业务中经常变成缺点。因为无状态意味着服务端不保存会话信息,令牌一旦签发,在有效期内就无法主动作废。
举几个真实场景:
- 用户修改密码后,旧 token 依然有效直到过期
- 管理员封禁了一个账号,该账号已签发的 token 还能继续访问接口
- 用户注销登录,token 只是在前端被删掉,如果被截获依然可用
- 多端登录管理要求“踢掉某台设备”,JWT 天生做不到
这些问题不是小概率事件,而是认证系统每天都要面对的需求。每种需求都得靠额外机制去弥补,比如维护黑名单、引入 token 版本号、缩短有效期配合刷新令牌。绕了这一大圈,服务端状态还是变相回来了,无状态的优势荡然无存。
2.3 主流方案的优势:可控性和安全性
反观 session + Redis 方案,每个需求都能直接命中:
- 踢人:删 Redis key
- 续期:刷新 TTL
- 改密码后失效全部会话:按用户维度删除所有会话 key
- 封号:上线校验时直接拒绝
另一个关键点是安全性。session ID 是一个随机字符串,本身不含任何业务信息,即使被截获,攻击者也无法从中提取用户信息。而 JWT 的 Payload 可读,虽然签名能防止篡改,但信息泄露风险始终存在。
Cookie 的 httpOnly 属性可以防止 XSS 攻击读取登录凭证,而 JWT 如果存在 localStorage 里,一旦站点被注入恶意脚本,token 直接就被偷走了。这是很多 JWT 安全事故的根源。
2.4 各方案的关键对比
| 维度 | JWT 无状态方案 | Session + Redis 方案 |
|---|---|---|
| 服务端存储 | 不需要 | 需要 |
| 主动失效 | 困难,需额外机制 | 容易,直接删除 |
| 跨域支持 | 天然友好 | 需配置 CORS 携带凭证 |
| 安全性 | Payload 可读,需谨慎 | 不透明,信息不泄露 |
| 扩展性 | 水平扩展友好 | 需共享 Redis |
| 体积 | 大,含业务声明 | 小,只是一个 ID |
| 移动端支持 | 友好 | 需处理 Cookie 或改为 token 头 |
这张表能帮你快速判断自己的场景适合哪种方案。核心判断标准就一条:需不需要服务端主动控制会话的生死。需要,就别硬上 JWT。
3. 实操过程与核心环节实现
3.1 如果非要用 JWT,怎么设计才不容易翻车
我前面说了不少 JWT 的问题,但这不代表 JWT 不能用。它适合的领域还是有的,关键是别拿它硬扛传统会话场景。如果你确实下了决心用 JWT,下面这套设计是我实际项目里打磨过的,能帮你少踩大部分坑。
第一步,选型算法。优先用 RS256 而不是 HS256。HS256 是对称签名,签发和验证用的是同一个密钥,在微服务架构里要把密钥分发到每个服务,泄露面太大。RS256 是非对称签名,私钥只在认证中心保存,各个业务服务只需要公钥验签,安全边界清晰。如果团队实在没有 RSA 密钥管理的经验,退一步用 HS256 也行,但密钥必须放在配置中心或环境变量里,绝对不能写进代码仓库。
第二步,缩短 access token 的有效期。我见过很多团队把 JWT 有效期设成 7 天甚至 30 天,这等于把风险敞口开到最大。建议 access token 有效期设成 15 分钟到 2 小时,视业务敏感度而定。配合 refresh token 实现无感续签,refresh token 用随机字符串,存储服务端,有效期可以长一些,比如 7 天或 30 天。
第三步,设计刷新接口。用户 access token 过期后,前端拿 refresh token 换新的 access token。这个接口要做三件事:校验 refresh token 是否存在且未过期、校验是否被吊销、签发新的 access token。refresh token 可以轮换,每次刷新都生成新的 refresh token,旧的立即失效,这样即使用户的 refresh token 泄露,最多只能用一次。
3.2 关于 token 续签的常见方案
续签是 JWT 落地时最容易被问起的问题。我整理过三种主流方案,各有优劣,看场景选。
第一种是固定有效期加前端定时刷新。用户在操作时,前端发现 token 快过期了就调用刷新接口换取新 token。实现简单,但有个问题:如果用户长时间不操作,token 到期后前端可能没有机会刷新,导致用户突然掉线。
第二种是滑动过期。用户每次请求时,如果 token 剩余有效期小于某个阈值,就在响应头里下发一个新的 token,前端自动替换。这种方案体验最好,但需要前后端配合,接口响应头里多带一个字段,前端做个拦截器统一处理。
第三种是双 token 方案。access token 用 JWT 且短期有效,refresh token 用不透明随机字符串存服务端。这种方案最稳定,也是我目前项目里的标配。refresh token 本质上回到了状态化方案,但它只占一个极窄的接口路径,不像全站 session 那样需要大规模存储。
3.3 如何防止数据被篡改
JWT 的签名机制天然能防篡改,但前提是你别在实现上出问题。我总结几个关键的注意事项:
- 严格校验
alg字段。历史上出现过攻击者把alg改成none绕过验签的案例,所以服务端必须白名单校验算法,不支持none,发现alg和预期不符直接拒绝。 - 密钥要足够长。HS256 之类的对称算法,密钥至少 32 字节,最好用随机生成的字节序列而不是一个单词。
- 验签时要对签名做恒定时间比较,防止时序攻击。虽然大多数现成库已经处理了这个问题,但如果你是手写验签逻辑,务必要注意。
- Payload 里要带
jti(JWT ID)声明。jti是唯一标识,一旦需要吊销某个 token,把它的jti加入黑名单即可,不用把整个 token 存下来。
3.4 模拟一个最小可用的 JWT 认证中心
这里给出一个最小实现骨架,用的 Node.js 和 jsonwebtoken 库,逻辑上任何语言都通用。你可以把它当模板,往自己的技术栈里迁移。
签发 token 的逻辑:
验证 token 中间件的逻辑:
刷新 token 的逻辑:
这样一套下来,JWT 负责无状态的短期验证,refresh token 负责可控的长期会话,两边的优势都拿到了。缺点是架构复杂度上来了,需要多维护一张 refresh token 存储表,但换来的是主动踢人、会话管理这些能力。
3.5 为什么我不建议把 JWT 存在 localStorage
这是很多前端容易踩的坑。为了方便调用接口,把 token 存在 localStorage 或 sessionStorage 里,看似没什么问题,但如果页面被注入了第三方脚本,攻击者直接就能通过 localStorage.getItem('token') 把凭证偷走。
正确做法是:access token 可以放在内存变量里,刷新页面后丢失,再靠 refresh token 静默续期。refresh token 放入 httpOnly 的 Cookie 中,加 Secure 和 SameSite=Lax 属性,防止 XSS 和 CSRF。这个方案在大多数主流框架里都能实现,代价是前端需要自己管理内存中的 access token,稍微麻烦一点,但安全收益明显。
提示:如果你们项目的安全要求不是特别变态,前端把 access token 放在内存、refresh token 放 httpOnly Cookie,够用了。别图省事把两个 token 都塞 localStorage。
4. 常见问题与排查技巧实录
4.1 JWT 常见漏洞总结
JWT 相关的安全漏洞我整理了几类,都是实际项目中见过或者圈子里公开的案例:
第一类是算法混淆漏洞。攻击者将 alg 改为 none 或改为 HS256,如果服务端没有校验算法白名单,攻击者就能用公钥当对称密钥来伪造 token。规避方法就是在验签时明确指定算法白名单,不依赖 token 头里的 alg 字段。
第二类是密钥泄露。既然是签名,密钥一泄露等于整个认证体系崩塌。建议用独立的密钥管理服务,或者至少把密钥放在环境变量里,定期轮换。代码仓库里出现密钥直接算安全事故处理。
第三类是 Payload 敏感信息泄露。JWT 的 Payload 可解码,很多团队习惯把用户手机号、邮箱放进去,这些信息一旦 token 被截获就暴露了。
第四类是过期时间形同虚设。有些 token 的 exp 字段被设置成 null 或者干脆不设置,这种 token 永不过期,风险极大。签发时务必强制设置 exp,验签时也要校验。
4.2 关于 token 续签的常见问题
问得最多的就是“用户一直用但 token 还是会过期,怎么办”。这个问题本质上不是技术问题,是产品策略问题。主流做法是滑动过期,用户有操作就续期,没操作就断开。具体来说,每次请求进来,如果 access token 剩余有效期少于 5 分钟,就自动签一个新的 access token 放到响应头 X-New-Token,前端拦截器检测到就替换本地 token。这样用户只要在活跃使用,就永远不会中途掉线。
另一个高频问题是“刷新接口被刷怎么办”。refresh token 轮换机制能解决一部分问题,但如果攻击者同时持有 access token 和 refresh token,刷新接口就成了无限续期口子。建议对刷新接口加频率限制和风险风控策略,比如检测到同一 refresh token 在很短时间内在不同 IP 上使用,直接吊销该用户所有 session,要求重新登录。
4.3 Session 方案与 JWT 方案混用是否可行
完全可行,而且很多大厂就是这么干的。前台用户中心用 session + Redis,对外开放平台接口用 JWT,后台管理系统用 session 或者更严格的 mTLS。混用不是技术上的妥协,而是根据不同场景选择最合适的方案。
我自己做过的一个项目中,C 端用户登录用的是 session + Cookie,移动端接口用 JWT,第三方开放平台的授权码用独立的一次性短码。三套认证逻辑同时运行,但由同一个认证中心统一管理,没有觉得混乱。关键是明确边界:JWT 只出现在需要无状态验证和跨域授权的路径上,其余一律走传统会话方案。
4.4 实测踩坑记录
第一次大规模使用 JWT 的时候,我把 access token 过期时间设成了 7 天,结果上线第二天就出事了。有个用户账号被盗,虽然我们及时重置了他的密码,但攻击者手里的旧 token 还能用,直到我们把 JWT 密钥换掉才彻底解决。这事的教训就一句话:JWT 的有效期就是你的安全响应时间窗口,设得越长,补救成本越高。
另一个踩过的坑是跨域问题。前端部署在不同域名下,携带 JWT 的请求要做 CORS 配置,Authorization 头属于非简单请求头,会触发预检请求。如果预检接口没做放行,前端就会莫名其妙地报跨域错误,而且报错信息很不直观。排查半天才发现是后端没处理 OPTIONS 请求。这个问题在用了 httpOnly Cookie 之后基本免疫了,因为 Cookie 不受同源策略限制,只需要配置 CORS 的 credentials。
4.5 如何从 Session 平滑迁移到 JWT
如果你现在用的是 session 方案,想逐步引入 JWT,不建议一次性全量切换。推荐的做法是灰度迁移:先在网关层同时兼容两种认证方式,根据用户 ID 哈希决定走哪种方案,观察一段时间的错误率和耗时时长,确认稳定后再把流量全量切过去。
迁移期间有两点要特别注意:一是双方案并存时,session 的 Redis 还能继续用,JWT 的吊销列表也要提前搭好,防止切过去之后出现踢不了人的尴尬。二是日志里要把两种认证方式打上不同的标签,出了问题能快速定位是哪种方案的问题。
5. 后续扩展思考
5.1 如果要做单点登录,JWT 是否合适
单点登录的核心是多个系统之间共享登录态。传统方案是 CAS 或者 SAML,流程复杂但可控性强。用 JWT 做 SSO 也可以,核心设计是认证中心签发一个 JWT,各个子系统用共享公钥验签,通过后建立自己的本地会话。
这种方式开发速度确实快,但有个坑:JWT 里的用户信息如果更新了,比如改了头像或者权限变化,其他子系统本地会话里的信息不会自动同步。要么每次请求都解析 JWT 拉最新用户信息,违背了减少查询的初衷,要么接受最长不短于 token 有效期的数据滞后。很多 JWT 做 SSO 的团队最后都不得不在 JWT 里只放用户 ID,其他信息全走接口查询,绕了一圈回到原点。
5.2 关于无状态认证的未来
其实无状态认证本身没有错,错的是拿它硬扛所有会话场景。现在比较前沿的做法是结合 OAuth2.1 和 OIDC 的标准流程,把短期 access token 和长期 refresh token 分开管理,让 JWT 只承担短周期内的无状态验证。这套做法已经是很成熟的行业共识了。
再往后走,服务端状态管理和客户端凭证的组合方式会越来越灵活,但底层的安全原则一直没变:凭证尽量短命、敏感信息不落地、服务端能主动控制会话生死。不管 JWT 还是 session,框架怎么变,这几条底线守住就不会出大问题。
5.3 我对 JWT 的最终看法
用了这么多年,我的结论是:JWT 是一个好工具,但不是万能的认证方案。它在适合自己的场景里非常好用,但在传统会话管理上确实不如 session 方案顺手。我不反对用 JWT,我反对的是不问场景无脑上 JWT。
如果你现在正面临认证技术选型,我的建议很直接:先梳理业务需求,把必须支持的场景列全,包括踢人、多端管理、改密码失效这些边缘场景,然后看 JWT 的方案需要加多少补丁才能满足这些需求。补丁的复杂度超过直接用 session 的复杂度,就老老实实用 session。如果你们的业务确实是无状态优先,比如开放 API、微服务网关,JWT 依然是一个合理的选择,只是记住别把有效期设太长。