3、令牌存储安全:前端存储方案对比与后端存储策略

令牌拿到手了,放哪儿?

这个问题,我见过太多团队栽跟头。说白了,存储位置选错了,前面做的所有安全防护都白搭。你想想看,一个精心保护的访问令牌,结果被随手扔在了一个能被 XSS 脚本轻松读取的地方——那感觉,就像把保险柜钥匙贴在门口。

3.1 前端存储三兄弟:Cookie、LocalStorage、SessionStorage

前端能存令牌的地方,基本就这三个。我一个个说,顺便聊聊我在项目里踩过的坑。

3.1.1 Cookie —— 老牌选手,但别裸用

Cookie 是浏览器自带的存储机制。每次请求都会自动带上,这对后端校验很方便。但方便归方便,坑也不少。

核心要点:Cookie 必须配合 HttpOnly、Secure、SameSite 三个属性使用,否则等于裸奔。

  • HttpOnly:禁止 JavaScript 读取 Cookie。这是防 XSS 的第一道防线。我见过一个团队没设这个,结果被 XSS 脚本直接把令牌偷走了。
  • Secure:只在 HTTPS 下传输。这个现在基本是标配了,但别忘了检查。
  • SameSite:控制跨站请求时是否携带 Cookie。建议设成 Strict 或 Lax,能有效防 CSRF。

我的习惯:如果是纯后端渲染的页面,我会优先用 HttpOnly Cookie 存令牌。前端完全碰不到它,安全系数高不少。

但 Cookie 也有短板。容量小(4KB 左右),而且容易受 CSRF 攻击。虽然 SameSite 能缓解,但老版本浏览器不支持。

3.1.2 LocalStorage —— 方便,但别存敏感信息

LocalStorage 容量大(5-10MB),API 简单,很多前端项目图省事就直接把令牌扔进去了。

嗯,这里要注意。LocalStorage 最大的问题是:任何 JavaScript 都能读取它。一旦你的页面被注入了 XSS 脚本,令牌就等于拱手送人。

我曾经接手过一个项目,他们把访问令牌和用户个人信息全存在 LocalStorage 里。结果一个第三方评论插件存在 XSS 漏洞,用户数据全被扒走了。那次教训让我再也不敢在 LocalStorage 里放敏感信息。

那 LocalStorage 适合存什么?我个人觉得,存一些非敏感的 UI 状态、主题偏好之类的还行。令牌?算了吧。

3.1.3 SessionStorage —— 比 LocalStorage 稍好,但有限

SessionStorage 和 LocalStorage 几乎一样,唯一的区别是:关闭标签页就自动清除。

这算是个优点——至少令牌不会长期留在浏览器里。但缺点也一样:XSS 面前毫无抵抗力。

我见过一些 SPA 应用用 SessionStorage 存令牌,配合短过期时间,算是折中方案。但说实话,这只能算「矮子里拔将军」,不是真正的安全方案。

3.2 前端存储方案对比表

特性 Cookie (HttpOnly) LocalStorage SessionStorage
XSS 防护 ✅ 强(JS 无法读取) ❌ 弱(JS 可直接读取) ❌ 弱(JS 可直接读取)
CSRF 防护 ⚠️ 需配合 SameSite ✅ 天然免疫 ✅ 天然免疫
容量限制 约 4KB 5-10MB 5-10MB
自动携带 ✅ 每次请求自动带 ❌ 需手动添加 ❌ 需手动添加
生命周期 可设置过期时间 手动清除 关闭标签页即清除
适用场景 后端渲染、BFF 模式 非敏感数据 短期会话、非敏感数据

3.3 后端存储策略

前端存令牌只是权宜之计。真正靠谱的做法,是让后端来管。

3.3.1 内存存储 —— 快,但重启就丢

把令牌存在后端内存里(比如 Node.js 的全局变量、Java 的 HashMap)。优点是快,缺点是服务重启后所有令牌失效,用户得重新登录。

我个人的建议是:开发环境可以用,生产环境别碰。除非你能接受所有用户同时掉线。

3.3.2 数据库存储 —— 持久化,但要注意性能

把令牌存在数据库里(Redis、MySQL、PostgreSQL 都行)。这样服务重启后令牌还在,用户体验好。

但这里有个坑:每次请求都要查一次数据库。高并发下,数据库压力会很大。

我的做法:用 Redis 存令牌,设置过期时间(TTL)。Redis 是内存数据库,读写快,而且 TTL 到期自动删除,省得自己写清理逻辑。

3.3.3 无状态 JWT —— 后端不存,全靠签名

JWT 本身就是自包含的。后端只需要验证签名,不需要查数据库。这在高并发场景下很香。

但无状态也意味着:你没法主动让一个令牌失效。除非你引入黑名单机制——那又变成有状态了。

我曾经在一个项目中用了纯无状态 JWT,结果用户改了密码后,旧令牌还能用半小时(因为还没过期)。后来我加了个 Redis 黑名单,才解决了这个问题。

3.4 知识体系:令牌存储安全全景图

下面这张图,我把前端和后端的存储方案串起来了。你可以看到,没有银弹,只有权衡。

令牌存储安全全景图 前端存储 Cookie (HttpOnly) ✅ JS 无法读取 ⚠️ 需配合 SameSite 防 CSRF 容量约 4KB LocalStorage ❌ JS 可直接读取 ✅ 天然防 CSRF 容量 5-10MB SessionStorage ❌ JS 可直接读取 ✅ 关闭标签页自动清除 容量 5-10MB 后端存储 内存存储 ✅ 读写极快 ❌ 服务重启令牌丢失 适合开发环境 数据库存储 (Redis) ✅ 持久化,支持 TTL ✅ 可主动失效令牌 ⚠️ 每次请求查库 无状态 JWT ✅ 无需查库,性能高 ❌ 无法主动失效 ⚠️ 需配合黑名单机制 没有安全的方案,只有根据场景权衡后的最佳选择

3.5 我的推荐方案

说了这么多,到底该怎么选?我直接给结论:

  1. 传统 Web 应用(后端渲染):用 HttpOnly Cookie + SameSite=Strict。后端用 Redis 存会话。这是最稳妥的方案。
  2. SPA 应用(前后端分离):用 BFF(Backend For Frontend)模式。令牌存在 BFF 层的 Session 里,前端只拿一个会话 ID。如果非要前端存,用 Cookie(HttpOnly)而不是 LocalStorage。
  3. 纯 API 服务(无前端):用无状态 JWT,配合短过期时间(15 分钟)和 Refresh 轮换机制。

记住一条铁律:令牌能往后端存,就别往前端放。前端只是展示层,安全责任应该由后端扛。

好了,这一章的内容就这些。存储方案选对了,你的令牌安全就成功了一半。剩下的,就是怎么传输和怎么刷新了——那是后面几章的事。