# JWT 和 Session 怎么选：撤销、扩展与安全边界

> 比较无状态 JWT 与服务端 Session 在撤销、扩展、XSS、CSRF 和移动端场景下的差异，给出可落地的选型建议。

**快速答案:** 需要即时撤销、以浏览器为主的应用优先用服务端 Session；需要跨服务无状态校验或支持移动端时，用短期 JWT 加 refresh token。两者本身没有绝对的安全高低，风险取决于存储位置、传输方式、撤销机制和有效期。

## 核心要点

- Session 把状态放在服务端，JWT 把签名后的声明放在客户端。
- Session 可以即时撤销；JWT 需要短期过期、轮换或撤销列表。
- 把 JWT 放进 localStorage 会暴露给 XSS；浏览器场景优先 HttpOnly Cookie。
- Cookie 认证需要 CSRF 防护；Authorization 头不自动发送，但主要风险变成 XSS。
- 短期 access token 加可撤销 refresh token 是常见的生产折中。

---

认证方案的讨论经常变成格式之争：JWT 还是 Session？更有用的问题是：状态放在哪里、如何撤销、凭证泄露后会发生什么。

这篇文章按生产系统真正关心的维度比较两种模型。

格式之争容易掩盖真正决定设计的问题：状态放在哪里、凭证泄露后多快失效、谁能读到声明、签名密钥泄露时影响多大。先把这几个问题回答清楚，Token 格式通常自然就定了。

## 服务端 Session 怎么工作

1. 用户提交账号密码。
2. 服务端创建会话记录和不透明 ID。
3. 浏览器通过 HttpOnly Cookie 保存 ID。
4. 每次请求，服务端查出会话并挂载用户。

客户端看不到会话内容。服务端删除记录就能让会话失效。代价是需要会话存储，并且每次请求都要查询。

会话 ID 必须不透明且不可预测：至少 128 位、来自加密安全随机源，不能是用户名、时间戳或自增序号。存储通常是 Redis 或数据库，查询会多一次网络往返，这是换取即时撤销和小体积 Cookie 的代价。会话记录应包含过期时间、用户 ID 和足够的元数据，用来支持“退出所有设备”。

Cookie 属性在这里很关键：`HttpOnly` 阻止脚本读取，`Secure` 防止明文传输，`SameSite=Lax` 挡住大多数跨站发送，`Path` 限制作用范围。登录成功后要重新生成会话 ID，防止会话固定；退出时既要清 Cookie，也要删除服务端记录。只清浏览器 Cookie、服务端会话仍然有效，是很常见的审计问题。

## JWT 怎么工作

1. 用户提交账号密码。
2. 服务端签发包含 `sub`、`role`、`exp` 等声明的 Token。
3. 客户端保存 Token，并在每次请求时发送。
4. 服务端验签并读取声明，不需要查询会话。

Token 自带信息。代价是撤销困难：只要签名有效且未过期，Token 就一直可用，除非服务端额外检查。

JWT 由三段 Base64URL 组成：头部如 `{"alg":"RS256","kid":"2026-11"}`，载荷如 `{"sub":"user_123","role":"admin","iss":"https://auth.example.com","aud":"api","exp":1762900000}`，以及覆盖前两段的签名。Base64URL 是编码，不是加密：拿到 Token 的人都能读声明，所以不要把密钥或敏感个人信息放进去。签名只有在验证方按白名单检查算法、按 `kid` 找到正确公钥，并校验 `iss`、`aud`、`exp`、`nbf` 之后，才能证明完整性和来源。漏掉这些检查，有效签名会带来虚假的安全感。

## 对比

| 维度 | 服务端 Session | JWT |
| --- | --- | --- |
| 状态位置 | 服务端 | 客户端 |
| 撤销 | 即时 | 需要额外状态 |
| 扩展 | 共享存储或粘性会话 | 无状态校验 |
| 浏览器存储 | HttpOnly Cookie | Cookie 或 localStorage |
| XSS 暴露 | 会话 ID 不可被 JS 读取 | localStorage 可被 JS 读取 |
| CSRF 暴露 | Cookie 场景存在 | 仅 Cookie 场景存在 |
| 移动端 | Cookie 容器或自定义头 | Bearer 头 |
| 微服务 | 共享会话存储 | 本地验签 |
| Token 体积 | 小 | 包含声明，较大 |
| 退出 | 服务端删除记录，立即失效 | 客户端删除副本，Token 仍然有效 |
| 密钥轮换 | 不涉及签名密钥 | 需要 JWKS 重叠和 `kid` 切换 |

有两行需要展开。第一，“退出”含义不同：删除会话记录立即终止访问；删除客户端上的 JWT，只是删除本地副本，复制过 Token 的人仍然能用。第二，Session 没有签名密钥，轮换几乎无成本；JWT 验签则依赖在旧 Token 过期前把新公钥分发出去。

## 撤销是核心决策

Session 删除记录即可撤销。JWT 要撤销，必须增加一种机制：

- **短期过期**：接受被盗 Token 在过期前仍可用。
- **Refresh token 轮换**：refresh token 存在服务端，可撤销；access token 保持短期。
- **Token version**：在声明里带版本号，每次请求和用户记录比较。
- **撤销列表**：保存已撤销的 Token ID，直到过期。

每种方案都会增加状态。如果每个请求都要求即时撤销，本质上是在用 JWT 重新实现 Session。

撤销不只针对退出登录。管理员被降级后，旧 Token 里的角色声明会一直有效到过期；重置密码不会让已签发的 Token 失效；笔记本丢失后，攻击者能用满整个有效期；员工离职后可能还有几小时访问权限。选 Token 有效期之前，先为每种事件写下可接受的最大撤销窗口。

最可靠的 JWT 撤销方案都在向 Session 借状态。Refresh token 轮换在服务端保存 token family；旧 refresh token 被重复使用时，服务端撤销整个 family 并强制重新登录。Token version 声明和用户记录比对，可以在改密码时一次性废掉所有 Token。用 `jti` 维护撤销列表也能工作，但列表会随签发量增长，还会重新引入你本来想避免的查询。按可接受的撤销窗口选机制，并把窗口写进文档。

## 扩展与微服务

多个服务需要本地校验身份、又不想共享 Session 存储时，JWT 很有吸引力。

代价是：

- 密钥分发和轮换变得关键。
- 签名密钥泄露会影响所有服务。
- 必须用算法白名单防止算法混淆。
- 声明在签发和过期之间可能变旧。

单一后端能维护状态时，Session 更简单。扩展需要 Redis 之类的共享存储，或者粘性会话。粘性会话会降低灵活性，也让发布更复杂。

JWT 的密钥管理有自己的故障模式：算法混淆。如果验证方接受 Token 头部里写的算法，攻击者可以诱导它接受 `alg: none`，或者用公钥当 HMAC 密钥去验证 HS256 签名。必须白名单限定允许的算法，绝不让头部决定验证方式；`kid` 查不到对应密钥时直接拒绝，不要回退到默认密钥。

密钥轮换需要重叠窗口。在启用新密钥签发之前，先把新公钥发布到 JWKS 端点；旧密钥要保留到最长有效期的 Token 全部过期；切换期间监控验签失败。签名密钥泄露比会话存储泄露更严重的一点是：所有信任该密钥的服务都会受影响，轮换会一次性作废所有未过期 Token。

## 浏览器存储：XSS 与 CSRF

JWT 放在 `localStorage` 时，页面上的任何 JavaScript 都能读取。一个 XSS 漏洞就可能泄露所有 Token。HttpOnly Cookie 不能被 JavaScript 读取，提高了窃取成本。

Cookie 会带来 CSRF 风险，因为浏览器自动发送。缓解手段包括 `SameSite=Lax` 或 `Strict`、CSRF Token 和来源校验。放在 `Authorization` 头里的 Token 不会自动发送，所以 CSRF 不是主要风险，XSS 才是。

没有一种存储方式绝对安全。选择取决于威胁模型，以及前端和 API 是否同站。

一个具体的 XSS 故障：评论框把用户输入当 HTML 渲染，攻击者注入脚本，脚本读取 `localStorage.getItem("token")` 并发到收集端点，攻击者拿到 Token 后可以用到过期。HttpOnly 去掉了这条读取路径，但没有消除 XSS：注入的脚本仍然能在页面里发起已认证请求，所以输入清洗和 CSP 仍然必要。

Cookie 认证不能只靠 `SameSite`。默认用 `SameSite=Lax`，高风险操作用 `Strict`，只有确实需要跨站流程时才用 `None; Secure`。对会改变状态的请求，再加一个和会话绑定的 CSRF Token，检查 `Origin` 和 `Referer`，并且不要用 GET 执行写操作。单页应用可以采用 BFF：Token 只存在服务端，浏览器只拿 HttpOnly 会话 Cookie，JavaScript 完全接触不到 Token。

## 移动端和第三方客户端

移动 App 和 CLI 没有浏览器的 Cookie 模型，Bearer Token 更自然。同一个后端同时服务浏览器和移动端时，常见做法是混合：

- 浏览器：HttpOnly 会话 Cookie，或短期 access token Cookie。
- 移动端：短期 access token 加 refresh token。
- 两端：服务端保存 refresh token 记录，用于撤销。

移动端还有自己的泄露路径。Token 被写进普通文件、日志、崩溃报告或备份，都可能从丢失的设备里被提取。应存进系统钥匙串或 Keystore，access token 尽量只放内存，refresh token 绑定设备标识，以便发现被复制的 Token。证书固定能提高中间人攻击成本，但会让轮换变复杂；没有更新固定证书的方案就不要启用。

## 实用的混合方案

很多生产系统使用：

1. 5 到 15 分钟的短期 access token。
2. refresh token 放在 HttpOnly Cookie 或安全存储。
3. 服务端保存 refresh token 记录，可撤销。
4. 每次刷新都轮换，并检测重复使用。
5. 校验 `exp` 时允许小的时钟容差。

这样大多数请求是无状态的，同时保留撤销路径。复杂度集中在刷新流程，而不是 access token。

最先撞上的竞态是并发刷新。两个标签页或两个接口调用同时刷新；如果轮换在第一次使用时就让旧 Token 失效，第二个请求会失败，甚至触发退出登录。解决办法包括：给旧 Token 一个很短的宽限窗口、每个会话加刷新锁，或者服务端对几秒内的重复请求返回同一个新 Token。重复使用检测必须区分这种良性竞态和真正被盗的 Token，否则用户正常浏览时也会被登出。

客户端能支持时，access token 应该只放内存。为了省事持久化到磁盘，会重新引入短期过期本来要缩小的窃取窗口。refresh token 才是长期凭证，值得最强的存储保护和最细的监控：换国家刷新、换设备刷新、改密后刷新，都应该告警。

## 选型建议

选择 **服务端 Session**：

- 需要即时退出和撤销。
- 应用以浏览器为主，由单一后端提供服务。
- 想要最简单的心智模型。
- 能运维会话存储。

选择 **JWT 加 refresh token**：

- 多个服务需要本地校验身份。
- 移动端或第三方客户端需要 Bearer Token。
- 能管理密钥轮换和短期过期。
- 能接受短暂的撤销窗口。

避免：

- 长期有效、没有撤销路径的 JWT。
- 把 JWT 放进 localStorage，却没有严格 CSP 和短期过期。
- 使用 Session Cookie 却没有 CSRF 防护。
- 验签之前就信任 JWT 声明。

| 需求 | 更合适的选择 | 原因 |
| --- | --- | --- |
| 退出必须立即生效 | Session | 服务端删除记录 |
| 上百个服务本地校验身份 | JWT | 本地验签 |
| 单一后端的浏览器应用 | Session | 攻击面更小 |
| 移动端和 CLI | JWT 加 refresh | Bearer 头更适合客户端 |
| 强多租户权限 | Session 或混合 | 每次请求拿到最新角色 |
| 离线验签 | JWT | 不查询存储 |

这个建议也会因为动机错误而失效。只因为 JWT 听起来现代就选它，却没有密钥轮换和撤销方案，会同时得到两种模型的缺点。只因为 Session 简单就选它，却又要给移动端提供公开 API，会把 Cookie 处理硬塞进原生应用。反过来，单一后端加 Redis 会话存储，往往比一整套 JWT 加 refresh token 平台更便宜；一个永远不会有第二个服务的系统，不值得引入分布式签名复杂度。

## 调试 Token

用 [JWT 解析工具](/tools/jwt-decoder) 查看声明，用 [JWT 签发工具](/tools/jwt-signer) 生成测试 Token。[密码哈希生成工具](/tools/password-hash) 可以测试凭据存储，[Basic 认证头生成工具](/tools/basic-auth) 适合旧接口。更完整的过期排查可以参考 JWT 过期指南。

Token 问题的检查清单：解码三段并确认载荷符合预期；用秒而不是毫秒比较 `exp` 和 `nbf` 与当前时间；确认 `aud` 是本服务、`iss` 是预期签发方；确认 `kid` 在密钥集合里存在；分别测试过期 Token、用错算法签发的 Token、被改过载荷的 Token。密钥正确却验签失败，通常是签名字节不一致，例如代理改写了空白，或者 Token 被 URL 解码了两次。

认证是一个系统：凭据存储、传输、会话生命周期、撤销、监控和用户体验。Token 格式只是系统中的一个选择，不是系统本身。

## 常见问题

### JWT 比 Session 更安全吗？

不能一概而论。JWT 存在 localStorage 时可能被 XSS 窃取，长期有效且无法撤销的 JWT 风险更大。Session Cookie 也可能被窃取，但服务端可以立即让会话失效。安全性取决于实现，而不是格式本身。

### JWT 可以撤销吗？

需要额外状态：短期过期、refresh token 轮换、撤销列表，或者用户记录上的 token version。完全无状态的 JWT 在过期前一直有效。

### 浏览器里 JWT 应该存在哪里？

前后端同站时，优先使用 HttpOnly、Secure、SameSite 的 Cookie。localStorage 可以被页面上的任何脚本读取，一个 XSS 漏洞就可能导致 Token 被窃取。如果必须用 localStorage，应缩短有效期并配置严格 CSP。

### 用 JWT 还需要 CSRF 防护吗？

如果 JWT 存在 Cookie 里并由浏览器自动发送，需要。CSRF 利用的就是自动携带 Cookie。如果 Token 由 JavaScript 放在 Authorization 头里发送，CSRF 不是主要风险，XSS 才是。

### 微服务应该用 JWT 还是 Session？

多个服务需要本地校验身份、又不想共享 Session 存储时，JWT 更方便。单一后端能维护会话状态时，Session 更简单。核心取舍是撤销能力和密钥管理。

## 参考资料

- [RFC 7519 - JSON Web Token (JWT)](https://www.rfc-editor.org/rfc/rfc7519)
- [OWASP - 会话管理备忘单](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html)
- [OWASP - JWT 安全备忘单](https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html)
- [MDN - Set-Cookie](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Set-Cookie)

---

- Canonical: https://www.jsonmind.com/zh-CN/guides/jwt-vs-session
- Locale: zh-CN
- Published: 2026-11-09
- Updated: 2026-11-09
- Tools: jwt-decoder, jwt-signer, password-hash, basic-auth