最近讨论 JWT 时,经常会出现两种相反的结论:一种认为 JWT 天然比 Session 更现代,另一种认为成熟网站都不该用 JWT。把这两个结论放在一起看,问题其实不在 JWT 本身,而在于大家把三个不同层次的东西放进了同一场比较。
Session 解决的是服务端如何保存登录状态;JWT 是一种携带声明的令牌格式;Opaque Token 则强调令牌内容对持有方不可见,资源服务器需要向授权服务查询状态。它们可以出现在同一个系统里,也可以组合使用,并不是三种互斥的登录技术。
真正决定方案的,不是"哪个更先进",而是四个问题:登录发生在浏览器还是 API 客户端?权限变化后要多快生效?资源服务器能不能独立验证?系统是否愿意为每次请求增加一次状态查询?把这四个问题回答清楚,选型就不会只剩下争论。
一、先把三个概念放回正确层级
先看最常见的服务端 Session。用户登录成功后,服务端创建一条会话记录,把用户 ID、登录时间、权限版本、过期时间等状态保存在服务端存储中。浏览器只保存一个随机的 Session ID,通常通过 Cookie 自动带回。服务端收到请求后,用这个 ID 找到会话,再决定是否允许访问。
JWT 的全称是 JSON Web Token。它把一组声明放进令牌中,并通过签名或加密等机制保护。常见的签名 JWT 可以让资源服务器在拿到令牌后,使用可信公钥验证签名,再检查过期时间、签发者和受众等字段。它的主要价值是把一部分验证所需的信息放在令牌里,而不是每次都查一条服务端会话记录。
Opaque Token 通常是一段没有可供客户端直接理解的随机字符串。资源服务器不能只靠解析字符串知道用户和权限,需要查询授权服务器或受控缓存,确认令牌是否有效、有哪些 Scope、是否过期或已撤销。OAuth 2.0 的 Token Introspection 规范就定义了这种查询方式。
因此,这三者不是同一维度的名词。Session 说的是状态存放在哪里;JWT 说的是令牌如何表达和验证;Opaque Token 说的是令牌对资源服务器是否透明。一个系统完全可以让浏览器使用 Session Cookie,让后端服务之间使用 JWT,再让第三方 API 使用 Opaque Access Token。
二、先看这张选型矩阵
| 评估维度 | 服务端 Session + Cookie | JWT Access Token | Opaque Access Token |
|---|---|---|---|
| 状态主要放在哪里 | 服务端会话存储 | 令牌声明,必要时仍需服务端状态 | 授权服务器或令牌存储 |
| 请求时如何验证 | 查 Session | 本地验签并检查声明 | 查询授权服务器或缓存 |
| 立即撤销能力 | 较直接,删除或禁用会话 | 需要额外撤销机制,或等待过期 | 可通过状态查询反映撤销 |
| 多资源服务器验证 | 共享会话存储或统一网关 | 可分发公钥并本地验证 | 依赖 Introspection 或共享缓存 |
| 每次请求的额外成本 | 会话存储查询 | 通常是本地密码学验证 | 通常有一次查询或缓存命中 |
| 浏览器端暴露面 | Cookie 可设 HttpOnly | 若令牌交给前端脚本,XSS 影响更大 | 令牌仍需安全保存,Opaque 不等于防盗 |
| 更适合的场景 | 同源 Web、后台管理、需要快速失效 | 多服务 API、需要本地验证 | 权限动态变化、撤销要求较强的 API |
这张表不是性能排名。Session 查询可能被 Redis 或本地缓存优化,Opaque Token 的 Introspection 也可以缓存;JWT 本地验签虽然省掉一次状态查询,但如果每个请求还要查用户权限、租户状态或撤销名单,它就不再是真正的无状态路径。
选型时还要区分访问令牌和刷新令牌。Access Token 用于访问资源,通常应该短时有效、权限范围尽量小;Refresh Token 用于换取新的 Access Token,权限更大、保存时间更长,保护要求也更高。不要因为 Access Token 使用 JWT,就顺手把 Refresh Token 也设计成长期可读、长期有效的 JWT。
三、浏览器网站优先解决 Cookie 和服务端状态
如果你的产品是普通 Web 网站,用户通过浏览器登录,前后端又属于同一站点或可控的几个域名,那么服务端 Session 往往是更简单的起点。登录成功后,服务端生成不可预测的会话 ID,把真实会话状态放在服务端;浏览器只保存 ID。用户退出、账号被禁用或权限发生重大变化时,服务端可以直接让对应会话失效。
Cookie 应明确设置安全属性,而不是依赖浏览器默认值。常见的会话 Cookie 可以采用如下形式:
http
Set-Cookie: __Host-session=<random-id>; Path=/; Secure; HttpOnly; SameSite=Lax
Secure 限制 Cookie 只通过 HTTPS 发送;HttpOnly 阻止普通页面脚本通过 document.cookie 读取它;SameSite 可以减少部分跨站请求携带 Cookie 的情况。SameSite 是纵深防御,不应被当成 CSRF 防护的唯一措施。还要检查登录、退出、修改密码和权限变更时的会话轮换与失效逻辑。
如果前端是 SPA,但后端可以提供一个同源 BFF(Backend for Frontend),可以让浏览器只持有 HttpOnly Cookie,由 BFF 在服务端保存和刷新 OAuth Token。浏览器不需要直接接触长期 Refresh Token,前端也不必把凭证放进 localStorage。OWASP 的 Session Management 指南明确提醒,不要把 Session ID、JWT、Access Token 或 Refresh Token 当作普通业务数据存进 localStorage 或 sessionStorage,因为同源 JavaScript 一旦受到 XSS 影响,就可能读取这些值。
这并不表示 Cookie 没有风险。浏览器会自动附带 Cookie,因此服务端仍要正确处理 CSRF、跨站请求、Cookie 域与路径范围、过期时间和会话固定问题。对于同源 Web,Session Cookie 的优势是服务端能控制状态,代价是需要维护会话存储和相关基础设施。
四、JWT 真正有价值的地方是跨服务本地验证
JWT 的优势出现在资源服务器较多、服务之间希望独立验证访问令牌的场景。授权服务器签发短时 JWT Access Token,资源服务器通过可信的密钥材料验证签名,再检查必要声明,就可以在本地完成一部分授权前置判断,不必为每个请求都回源查询会话。
但"能验签"不等于"可以直接放行"。验证时至少要固定允许的算法集合,不能照单全收令牌头里的 alg;要验证 iss 是否属于可信签发者,aud 是否包含当前资源服务器,exp 是否尚未过期,并根据业务检查 scope、sub、tenant 等字段。不同用途的 JWT 还应使用相互独立的验证规则,避免把一个场景签发的令牌拿到另一个场景使用。
JWT 的签名也不等于加密。普通签名 JWT 的 Payload 通常可以被解码读取,不应把密码、密钥、身份证号等敏感内容塞进去。签名主要用于完整性和来源验证,不负责隐藏内容。RFC 8725 专门列出了算法混淆、弱密钥、Issuer 和 Audience 校验缺失、不同用途令牌混用等实现风险。
真正容易被低估的是撤销问题。假设用户刚刚被管理员禁用,某个已经签发、尚未过期的 JWT 仍可能通过本地验签。若业务要求权限变化立即生效,就必须增加额外机制,例如缩短令牌有效期、维护撤销列表、检查用户权限版本,或在关键操作前回到授权服务确认状态。每增加一次状态查询,都会重新引入一致性、缓存和可用性成本。
所以 JWT 不是"完全无状态"的同义词。它只是让资源服务器可以在某些条件下本地验证令牌。只要业务还需要即时禁用、租户隔离、动态角色或设备撤销,就要明确这些状态由谁保存、多久同步、缓存失效如何处理。
五、Opaque Token 用查询成本换取状态控制
Opaque Access Token 的思路相反:令牌本身不承担完整的权限表达,资源服务器拿到它以后,向授权服务器的 Introspection Endpoint 查询当前状态。授权服务可以返回 active、scope、client_id、sub、aud、exp 等信息,资源服务器根据结果决定是否放行。
它的优势是撤销和权限变化更容易集中控制。管理员撤销令牌后,后续查询可以返回 active=false;授权服务也可以根据当前账号状态、令牌范围和目标资源作出判断。对于权限变化频繁、需要集中审计或资源服务器不能自行维护撤销状态的系统,这种方式有实际价值。
代价也很明确:资源服务器多依赖一个网络服务。授权服务器变慢或不可用时,资源访问会受到影响;每次都查询会增加延迟和负载;缓存 Introspection 结果虽然能减轻压力,却会形成一段撤销信息尚未同步的窗口。RFC 7662 明确要求设计者考虑缓存时长与安全性之间的取舍,不能只把缓存打开就当成性能优化完成。
Opaque Token 也不是天然比 JWT 安全。只要令牌被窃取,攻击者仍可能在令牌有效期间使用它;它只是让服务端更容易控制令牌状态。传输、存储、Scope 限制、客户端认证、访问日志脱敏和令牌轮换仍然需要完整设计。
六、按四个问题走完选型路线
先问客户端是谁。如果是普通浏览器网站,并且有自己的后端,优先评估 Session Cookie 或 BFF。浏览器只持有短小的会话凭证,服务端保留真正的登录状态。若是移动端、桌面端或第三方 API 客户端,再按 OAuth 客户端类型和资源服务器结构选择 Access Token。
第二个问题是撤销时效。用户退出后只要求当前浏览器会话失效,服务端 Session 很直接;管理员禁用账号后要求所有 API 尽快停止访问,Opaque Token 加 Introspection 更容易集中控制;如果能接受短时间的令牌有效窗口,JWT 配合较短过期时间也可以成立。不要把"调用了撤销接口"误认为所有资源服务器已经立刻拒绝旧 JWT。
第三个问题是资源服务器能否独立验证。如果资源服务器分布在多个服务或边缘节点,希望不依赖每次回源,就考虑 JWT 本地验签。但前提是密钥分发、轮换、Issuer、Audience、Scope 和过期时间都有明确规则。如果这些条件没有准备好,JWT 只会把复杂度从请求路径搬到密钥管理和故障排查里。
第四个问题是前端是否必须直接持有 Token。能通过 BFF 代理就不必把长期凭证交给浏览器脚本;必须由公共客户端持有令牌时,应采用符合 OAuth 安全最佳实践的授权流程。RFC 9700 对公共客户端的 Refresh Token 保护提出了明确要求:使用发送方约束令牌,或采用 Refresh Token Rotation 来检测重放。不要自己发明一个永不过期的刷新凭证。
把路线压缩成下面这张图:
text
先看客户端
├─ 浏览器 Web
│ ├─ 有自己的后端 / BFF → Session Cookie,Token 留在服务端
│ └─ 必须前端直连 API → 按 OAuth 公共客户端规则设计
└─ 移动端 / 桌面端 / 第三方 API
├─ 需要集中撤销、动态权限 → Opaque Token + Introspection
└─ 需要资源服务器本地验证 → 短时 JWT + 严格声明校验
这不是绝对规则,而是把最关键的架构成本提前摆出来。选 JWT 需要承担密钥和撤销设计;选 Opaque Token 需要承担授权服务查询与缓存一致性;选 Session 需要承担服务端会话存储和跨服务共享。没有哪一种方案能把这些成本全部消掉。
七、三个常见项目可以这样落地
小型后台管理系统:先使用服务端 Session 和安全 Cookie。会话记录放在服务端存储,登录后轮换 Session ID,退出和密码变更时使会话失效。不要为了"看起来分布式"就把每个浏览器会话改造成 JWT。
SPA 加自有后端:优先考虑 BFF。浏览器通过 HttpOnly Cookie 访问 BFF,BFF 保存 OAuth Token 并代为访问下游 API。这样可以减少长期令牌暴露在浏览器 JavaScript 环境中的机会。若确实需要跨站调用,先把 Cookie、CSRF、CORS 和刷新流程设计完整,不要把 localStorage 当作默认答案。
多服务 API 平台:如果资源服务器数量多、可以稳定分发公钥,并且接受短时令牌的撤销窗口,JWT Access Token 是合理选择;如果权限需要集中即时收回,Opaque Token 加 Introspection 更容易实现。也可以按风险分层:普通读接口使用短时 JWT,高风险操作额外检查账号状态或权限版本。混合方案必须明确哪些接口走本地验证、哪些接口必须实时检查,避免不同服务对同一令牌作出不同判断。
最后再强调一次:认证方案的目标不是让每个请求都少查一次数据库,而是让系统在正常请求、用户退出、权限变更、密钥轮换和服务故障时,都能给出一致且可解释的结果。真正的成本不是 JWT 多几个字段,而是当你需要撤销、审计和跨服务一致性时,系统是否已经为这些事情留好了位置。
如果你正在做一个真实项目,你更难解决的是"用户退出后立即失效",还是"多个资源服务器不想每次回源"?把客户端类型、服务数量和撤销要求说清楚,通常比先争论 JWT 好不好更容易得到可执行的答案。