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 知识体系:令牌存储安全全景图
下面这张图,我把前端和后端的存储方案串起来了。你可以看到,没有银弹,只有权衡。
3.5 我的推荐方案
说了这么多,到底该怎么选?我直接给结论:
- 传统 Web 应用(后端渲染):用 HttpOnly Cookie + SameSite=Strict。后端用 Redis 存会话。这是最稳妥的方案。
- SPA 应用(前后端分离):用 BFF(Backend For Frontend)模式。令牌存在 BFF 层的 Session 里,前端只拿一个会话 ID。如果非要前端存,用 Cookie(HttpOnly)而不是 LocalStorage。
- 纯 API 服务(无前端):用无状态 JWT,配合短过期时间(15 分钟)和 Refresh 轮换机制。
记住一条铁律:令牌能往后端存,就别往前端放。前端只是展示层,安全责任应该由后端扛。
好了,这一章的内容就这些。存储方案选对了,你的令牌安全就成功了一半。剩下的,就是怎么传输和怎么刷新了——那是后面几章的事。