正在加载,请稍候…

JWT 与 Session Token:你应该使用哪种身份验证方法?

JWT 与基于会话的身份验证的实用比较——涵盖各自的工作原理、安全权衡、可扩展性影响以及何时选择一种而非另一种。

JWT 与 Session Token:你应该使用哪种身份验证方法?

两种证明身份的方式

用户使用用户名和密码登录后,服务器需要一种方式来识别后续请求中的该用户——因为 HTTP 是无状态的。每个请求到达时都没有对之前请求的固有记忆。

解决这个问题有两种主流方法:

  1. Session Token——服务器创建登录记录并存储,然后给客户端一个引用键(会话 ID),供其在后续请求中出示。
  2. JWT(JSON Web Token)——服务器创建一个包含用户身份和声明的自包含令牌,签名后交给客户端。无需服务器端存储。

两种方法都可行,但各有不同的权衡。

JWT 与 Session Token:你应该使用哪种身份验证方法?插图

基于会话的身份验证如何工作

  1. 用户向 POST /login 发送凭据
  2. 服务器验证凭据并在存储(数据库、Redis、内存)中创建会话记录
  3. 服务器将会话 ID 作为 cookie 返回(通常为 HttpOnly; Secure; SameSite=Strict)
  4. 浏览器自动将 cookie 附加到每个后续请求
  5. 服务器在每个请求上查找存储中的会话 ID 以识别用户
  6. 要注销,服务器删除会话记录——会话 ID 立即失效

会话 ID 本身毫无意义——它是一个随机的不透明字符串,如 sess_a3f9d2c8b4。所有实际用户数据都存储在服务器上。

JWT 身份验证如何工作

  1. 用户向 POST /login 发送凭据
  2. 服务器验证凭据并创建包含声明(用户 ID、角色、过期时间)的 JWT
  3. 服务器使用密钥(HMAC)或私钥(RSA/ECDSA)对 JWT 进行签名
  4. 服务器返回 JWT——客户端将其存储在内存、localStorage 或 cookie 中
  5. 客户端在每个请求的 Authorization: Bearer <token> 头中发送 JWT
  6. 服务器验证签名并读取声明,无需任何存储查找
  7. 要“注销”,客户端丢弃令牌——但令牌在 exp 声明过期之前仍然有效

解码后的 JWT 负载如下所示:

{
  "sub": "user_12345",
  "email": "alice@example.com",
  "roles": ["user", "editor"],
  "iat": 1716800000,
  "exp": 1716886400
}

令牌外部的签名确保此负载未被篡改。

并排比较

属性 Session Token JWT
服务器端存储 需要(DB、Redis) 不需要
即时撤销 是——删除会话 否——必须等待过期
可扩展性 多服务器设置中需要共享存储 无需协调即可在任何服务器上工作
令牌大小 小(随机 ID,约 20–40 字节) 较大(编码后的声明,约 200–600 字节)
客户端可读负载 否 是(base64 解码,但非秘密)
最佳传输方式 HttpOnly cookie Authorization 头或 HttpOnly cookie
注销效果 完全 仅移除客户端副本
复杂性 实现和推理简单 需要理解 JWT 规范

JWT 的撤销问题

这是最常被误解的权衡。JWT 在过期前一直有效。如果用户账户被入侵,或者管理员需要立即使令牌失效,则没有服务器端记录可删除。

存在解决方案,但每个都会增加复杂性:

短过期时间 + 刷新令牌——签发 15 分钟过期的访问令牌和数天或数周过期的刷新令牌。被入侵的访问令牌很快失效。这是最广泛推荐的模式。

令牌黑名单——维护一个已失效 JWT ID(jti 声明)的数据库。在每个请求上检查它。这又带回了服务器端存储——但现在只存储例外情况,而不是所有会话。

用户记录中的版本字段——在 JWT 中包含一个 token_version 声明。将当前版本存储在用户记录中。在注销或更改密码时递增。拒绝版本较低的令牌。每个请求需要一次数据库读取。

对于即时撤销至关重要的应用(金融服务、安全敏感的管理工具),会话令牌更简单且更可预测。

JWT 与 Session Token:你应该使用哪种身份验证方法?插图

可扩展性论证

JWT 最常被引用的优势是水平可扩展性。使用会话时,每个请求都需要访问会话存储。在多服务器设置中,这意味着要么使用粘性会话(将每个用户路由到同一台服务器),要么使用共享的外部存储(如 Redis)。

使用 JWT,任何服务器都可以验证任何令牌而无需协调——只需检查签名。这确实简化了分布式架构。

但实际差异通常比声称的要小。Redis 每秒处理数百万次查找。对于大多数应用,会话存储并非瓶颈。可扩展性论证在非常大规模或 serverless/边缘计算环境中最为重要,因为在这些环境中全局状态确实难以共享。

安全考虑

JWT 存储

JWT 的存储位置至关重要。

localStorage——实现简单,但易受 XSS 攻击。页面上的任何 JavaScript(包括第三方脚本)都可以读取 localStorage。如果你的应用存在 XSS 漏洞,攻击者会窃取每个用户的令牌。

sessionStorage——与 localStorage 相同的 XSS 风险,但令牌在标签页关闭时被清除。

HttpOnly cookie——浏览器自动处理 cookie,JavaScript 无法读取。这是 Web 应用最安全的选择。结合 SameSite=Strict 或 CSRF 令牌时也能抵抗 CSRF。

内存(JavaScript 变量)——抗 XSS,但令牌在页面重新加载时丢失。与 HttpOnly cookie 中的刷新令牌配合使用效果良好。

建议:对于 Web 应用,将 JWT 存储在 HttpOnly cookie 中。对于需要客户端读取声明的单页应用,将 JWT 存储在内存中,并将刷新令牌存储在 cookie 中。

算法选择

验证 JWT 时始终指定预期的算法。历史上存在一个漏洞,允许攻击者将算法更改为 alg: none,从而完全绕过签名验证。现代 JWT 库如果正确配置,可以处理此问题:

// Node.js (jsonwebtoken)
jwt.verify(token, secret, { algorithms: ['HS256'] }); // 始终指定

// 永远不要这样做——容易受到算法混淆攻击
jwt.verify(token, secret);

JWT 与 Session Token:你应该使用哪种身份验证方法?插图

短过期时间

访问令牌应在 15–60 分钟内过期。刷新令牌在 7–30 天内过期并轮换。长期有效的访问令牌是最常见的 JWT 安全错误。

何时使用会话

  • 传统的服务器渲染 Web 应用(Django、Rails、Laravel)
  • 需要即时注销或账户终止的应用
  • 令牌中的用户数据较大或频繁变化的应用
  • 当简单性和可调试性比无状态可扩展性更重要时

何时使用 JWT

  • 移动应用使用的 API(无自动 cookie 处理)
  • 微服务,其中多个服务需要验证身份而无需访问中央数据库
  • Serverless 和边缘计算环境,其中全局状态成本高昂
  • 单点登录(SSO)场景,其中令牌跨域边界

混合方法

许多生产系统同时使用两者。会话 cookie 处理 Web 浏览器登录。JWT 作为短期访问令牌用于 API 调用,特别是来自移动客户端或第三方集成。刷新令牌——存储在安全的 HttpOnly cookie 中——允许无需重新登录即可透明续期。

这种组合为你提供了服务器端会话管理的主要登录安全性、JWT 的 API 授权无状态可扩展性,以及通过会话/刷新令牌实现的即时撤销。

→ 使用 JWT 解析器 解码和检查任何 JWT 令牌,读取其声明并检查其过期时间——无需密钥。