
两种证明身份的方式
用户使用用户名和密码登录后,服务器需要一种方式来识别后续请求中的该用户——因为 HTTP 是无状态的。每个请求到达时都没有对之前请求的固有记忆。
解决这个问题有两种主流方法:
- Session Token——服务器创建登录记录并存储,然后给客户端一个引用键(会话 ID),供其在后续请求中出示。
- JWT(JSON Web Token)——服务器创建一个包含用户身份和声明的自包含令牌,签名后交给客户端。无需服务器端存储。
两种方法都可行,但各有不同的权衡。

基于会话的身份验证如何工作
- 用户向
POST /login发送凭据 - 服务器验证凭据并在存储(数据库、Redis、内存)中创建会话记录
- 服务器将会话 ID 作为 cookie 返回(通常为
HttpOnly; Secure; SameSite=Strict) - 浏览器自动将 cookie 附加到每个后续请求
- 服务器在每个请求上查找存储中的会话 ID 以识别用户
- 要注销,服务器删除会话记录——会话 ID 立即失效
会话 ID 本身毫无意义——它是一个随机的不透明字符串,如 sess_a3f9d2c8b4。所有实际用户数据都存储在服务器上。
JWT 身份验证如何工作
- 用户向
POST /login发送凭据 - 服务器验证凭据并创建包含声明(用户 ID、角色、过期时间)的 JWT
- 服务器使用密钥(HMAC)或私钥(RSA/ECDSA)对 JWT 进行签名
- 服务器返回 JWT——客户端将其存储在内存、localStorage 或 cookie 中
- 客户端在每个请求的
Authorization: Bearer <token>头中发送 JWT - 服务器验证签名并读取声明,无需任何存储查找
- 要“注销”,客户端丢弃令牌——但令牌在
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 最常被引用的优势是水平可扩展性。使用会话时,每个请求都需要访问会话存储。在多服务器设置中,这意味着要么使用粘性会话(将每个用户路由到同一台服务器),要么使用共享的外部存储(如 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);

短过期时间
访问令牌应在 15–60 分钟内过期。刷新令牌在 7–30 天内过期并轮换。长期有效的访问令牌是最常见的 JWT 安全错误。
何时使用会话
- 传统的服务器渲染 Web 应用(Django、Rails、Laravel)
- 需要即时注销或账户终止的应用
- 令牌中的用户数据较大或频繁变化的应用
- 当简单性和可调试性比无状态可扩展性更重要时
何时使用 JWT
- 移动应用使用的 API(无自动 cookie 处理)
- 微服务,其中多个服务需要验证身份而无需访问中央数据库
- Serverless 和边缘计算环境,其中全局状态成本高昂
- 单点登录(SSO)场景,其中令牌跨域边界
混合方法
许多生产系统同时使用两者。会话 cookie 处理 Web 浏览器登录。JWT 作为短期访问令牌用于 API 调用,特别是来自移动客户端或第三方集成。刷新令牌——存储在安全的 HttpOnly cookie 中——允许无需重新登录即可透明续期。
这种组合为你提供了服务器端会话管理的主要登录安全性、JWT 的 API 授权无状态可扩展性,以及通过会话/刷新令牌实现的即时撤销。
→ 使用 JWT 解析器 解码和检查任何 JWT 令牌,读取其声明并检查其过期时间——无需密钥。