也说 JWT
网上关于 JWT(JSON Web Token)的技术文章汗牛充栋,但大部分教程都在重复着同一种看似完美的叙事:
“传统的 Session 需要服务器保存状态,集群部署时需要做 Session 共享;而 JWT 最大的优势就是服务端完全无状态,所有数据加密存放在客户端,服务器无需查库,仅凭密码学签名即可完成鉴权,横向扩容极度丝滑……”
当年初学后端时,我也曾被这种“完全无状态”的美好愿景所吸引。然而在真正做过企业级鉴权中心与 OAuth2.0 网关之后,我得出了一个完全相反的结论:
在真实的生产业务中,追求“绝对无状态”的 JWT 几乎是一个灾难;服务端如果对 Token 彻底失去状态感知,系统将处于巨大的安全失控状态。
本文深入聊聊为什么我认为“服务端必须掌握状态”,以及 JWT 在工业界中的真实落地姿势。
一、JWT 到底是什么?
根据 RFC 7519 官方规范,JWT 是一种开放标准,定义了一种紧凑且**自包含(Self-Contained)**的信息传输格式。
一个标准的 JWT 由三部分组成,通过英文句点 . 拼接:
1 | |
1. Header(头部)
声明令牌类型和所使用的签名算法(如 {"alg": "HS256", "typ": "JWT"})。
2. Payload(载荷)
存放实际业务数据(Claims),例如用户 ID、角色以及内置字段(如过期时间 exp)。
⚠️ 避坑认知: Payload 只是经过了
Base64Url编码,而不是加密! 任何人在浏览器抓包或拿到字符串后,只需点一下atob()就能明文看光所有字段。因此,绝对严禁在 Payload 里存放用户密码、手机号身份证等隐私敏感信息。
3. Signature(防篡改签名)
1 | |
签名是利用服务端保管的私钥(secret_key),对前两部分做哈希计算得出的校验码。如果黑客在客户端偷偷把 Payload 里的 role: "user" 改成了 admin,只要他不知道私钥,算出来的签名就无法匹配,服务端便能立刻识别出数据被篡改。
二、致命的“无状态悖论”:为什么服务端必须掌握状态?
很多文章宣称“有了 JWT,服务端就可以把 Redis 和 Session 删了”。我们不妨推演以下几个在任何商业系统里都必须处理的场景:
场景 1:用户主动点击“退出登录”
- 签发给客户端的 JWT 有效期为 2 小时;
- 用户在第 10 分钟点击了“退出登录”,前端清除了 LocalStorage 里的 Token;
- 致命漏洞:如果该 Token 在退出前被恶意脚本窃取、或被员工自己从控制台复制出来,在接下来的 1 小时 50 分钟内,持有该 Token 的任何人依然可以畅通无阻地调用你的所有 API!
- 因为对于无状态的 JWT 来说,签发即放行,只要没到
exp设定时间,签发者自己都无法提前注销它。
场景 2:修改密码 / 强制踢人下线
- 用户发现账号被异地盗用,紧急修改了密码;
- 此时黑客手里的旧 Token 依然合法有效,照样能拿着旧 Token 继续转账或下载私密资料,直到自然过期;
- 管理员发现某账号是内鬼,在后台将其“封禁(Disable)”,但该内鬼在 Token 过期前依然拥有管理员权限。
场景 3:权限动态变更延迟
- 某普通员工晋升为主管,后台授予了审批权限;但由于旧 Token 中固化了
role: "user",他必须先登出重新登录生成新 Token; - 反之,降权更是灾难:撤销了某人的权限,旧 Token 里的特权却依然有效。
三、无状态与强管控的博弈
通过上述推演,你会发现一个后端架构的死结(Paradox):
1 | |
如果你在服务端搞了个 Redis 黑名单(只要用户退出登录就把 token 塞进 Redis,每次请求都去查 Redis 校验),那这和传统的 Session / Redis Token 还有什么区别? 甚至它的代价比 Session 更大——不仅没省下 Redis 查询,每次 HTTP 请求还要白白在网络上传输大几十倍体积的 Base64 字符串!
四、JWT 的正确打开姿势:工业界成熟架构方案
既然纯无状态 JWT 弊端明显,那么它在各大厂的生产系统里到底是怎么用的?
最佳实践 1:双 Token 机制(短命 Access Token + 长命 Refresh Token)
这是目前工业界(包括 OAuth 2.0、GitHub、Google 等)最标准的解法。我们可以通过泳道图(Swimlane Sequence Diagram)清晰看懂客户端、鉴权网关、Redis 状态库与微服务之间的协作全生命周期:
sequenceDiagram
autonumber
participant Client as 客户端
participant Gateway as API 网关 / 鉴权中心
participant Redis as Redis 令牌状态库
participant Service as 业务微服务集群
Note over Client, Service: 阶段 1:用户登录与双 Token 签发
Client->>Gateway: POST /login (用户名与密码)
Gateway->>Gateway: 验证身份并生成短命 Access Token
Gateway->>Redis: 记录长命 Refresh Token (7天 TTL)
Redis-->>Gateway: 存储成功
Gateway-->>Client: 返回 Access Token 与 Refresh Token
Note over Client, Service: 阶段 2:常规业务请求 (纯无状态,极速放行)
Client->>Gateway: GET /api/orders (携带 Access Token)
Gateway->>Gateway: 本地公钥验签与过期时间校验 (不查 Redis)
Gateway->>Service: 验签通过,转发请求 (携带解析后的用户信息)
Service-->>Client: 返回业务数据
Note over Client, Service: 阶段 3:Access Token 自然过期与无感静默刷新
Client->>Gateway: GET /api/orders (Access Token 已过期)
Gateway-->>Client: 401 Unauthorized
Client->>Gateway: POST /refresh (携带 Refresh Token)
Gateway->>Redis: 校验 Refresh Token 是否有效
Redis-->>Gateway: 校验通过 (未被吊销)
Gateway->>Gateway: 签发新的 Access Token
Gateway-->>Client: 返回新 Access Token
Client->>Gateway: 重试发起业务请求
Gateway->>Service: 转发请求
Service-->>Client: 返回业务数据
Note over Client, Service: 阶段 4:用户登出 / 修改密码 / 强制下线 (服务端主动管控)
Client->>Gateway: POST /logout (主动登出)
Gateway->>Redis: 立即删除对应 Refresh Token
Redis-->>Gateway: 删除成功
Gateway-->>Client: 200 OK 登出成功
Note over Client, Redis: 此时即使旧 Access Token 泄露,最大漏洞窗口仅剩余几分钟,且彻底无法续期!
- 正常业务高并发访问:带着有效期只有 5~15 分钟的 Access Token(无状态校验),微服务网关凭借本地公钥即可快速验签,享受无状态的高性能;
- 需要撤销与安全下线:用户点击登出、修改密码或被封号时,服务端直接在 Redis 中注销其 Refresh Token。这样,即使被盗的 Access Token 还有几分钟寿命,在十几分钟后它将因无法换发新 Token 而彻底暴毙,将安全损失窗口强行压缩在可控的数分钟之内。
最佳实践 2:内网微服务间身份传递(Token 降级模式)
- 在对外暴露的 API 网关层,使用传统的 Session 或轻量 Opaque Token(随机 32 位字符串),网关持有状态;
- 网关校验通过后,在内部向各个下游微服务(如订单服务、库存服务、风控服务)转发时,动态签发一个极短有效期的内部 JWT(携带
userId、roles、tenants); - 内部微服务拿到 JWT 之后直接反序列化拿到上下文信息,无需跨服务反查用户中心。这种模式下,JWT 充当的是安全的内部跨服务上下文传递载体:
flowchart LR
subgraph ClientLane [客户端]
Client["前端应用 / APP"]
end
subgraph GatewayLane [网关与鉴权]
Gateway["API 网关 (状态校验)"]
Redis[("Redis Session")]
end
subgraph ServiceLane [后端微服务集群]
OrderService["订单微服务"]
UserService["用户微服务"]
PayService["支付微服务"]
end
Client --> Gateway
Gateway <--> Redis
Gateway --> OrderService
Gateway -.-> UserService
Gateway -.-> PayService
最佳实践 3:短周期的临时委托与文件下载鉴权
比如生成一个“邮件激活链接”、“找回密码验证串”或“OSS 临时私有文件下载 URL”,设置有效期 5 分钟,将文件路径与有效时间签名成 JWT 拼接在 URL 参数后。这种场景完全不需要入库,天然契合 JWT 的无状态自包含特性。
五、JWT 与普通 Token 的本质区别
很多人分不清 JWT 与普通 Token(如 UUID / 随机哈希串)的区别,一张表看懂:
| 比较维度 | 传统 Token (Opaque Token / Session ID) | JWT (Self-Contained Token) |
|---|---|---|
| 形态本质 | 一串无意义的随机字符串(如 f47ac10b...) | 结构化的明文数据包 + 数字签名 |
| 数据承载 | 不承载任何数据,只是一个“取件码” | 自带用户 ID、角色、有效期等业务属性(自包含) |
| 鉴权方式 | 服务端必须拿着取件码去 Redis/数据库查询具体信息 | 仅通过算法验证签名是否合法即可信任里面的数据 |
| 主动注销 | 极度容易(直接删掉 Redis 对应的 Key 即可) | 极度困难(除非引入黑名单破坏无状态) |
| 网络开销 | 极小(通常 32 ~ 64 字节) | 较大(通常 500 ~ 2000 字节,增大 Header 开销) |
总结
当年阮一峰老师的文章确实推动了国内开发者对 JWT 的认知普及,但“所有数据都存在客户端,服务器完全不存”的理念,更偏向于理想化的学术模型。
软件架构从来没有银弹,只有权衡(Trade-off):
- 不要为了“时髦”而盲目在单体或普通 Web 项目中抛弃 Session 改用纯无状态 JWT;
- 接受客户端永远不可信的现实。在涉及用户核心权限、金钱交易与登出控制的场景下,服务端保留对状态的终极控制权,永远是系统安全最坚实的一道防线。
参考资料与延伸阅读
1. IETF 核心协议标准规范
- 📖 RFC 7519: JSON Web Token (JWT) — JWT 核心数据格式、声明(Claims)与密码学规范
- 📖 RFC 6749: The OAuth 2.0 Authorization Framework — 双 Token 体系(Access Token 与 Refresh Token)标准架构
- 🛡️ RFC 8725: JSON Web Token Best Current Practices (BCP) — 官方发布的 JWT 安全使用最佳实践(防
alg: none攻击与弱密钥)
2. 业界深度分析与安全指引
- 🛡️ OWASP Cheat Sheet Series: JSON Web Token for Java/Node/Go — 权威安全机构的防重放与令牌吊销建议
- 📝 Stop using JWT for sessions (Redis / Session vs JWT) — 密码学领域著名的无状态批判文章
- 🛠️ jwt.io (Auth0) — 官方在线 Debugger 与各主流语言 JWT SDK 选型库
