前端圈子里有一个吵了十年也没吵出结论的问题:登录后拿到的那个令牌,到底该存在哪。localStorage存取方便,httpOnly cookie更安全但配置麻烦,两派各有拥趸。真正值得关注的不是这个老问题本身,而是一个反常识的事实:连Auth0Okta这两家把"专业身份认证"当卖点的头部服务商,自家SPA默认方案都没有站在安全教科书推荐的那一边。

localStorage的问题不在"会不会被攻破",而在"攻破后能拿走多少"

最常见的教程写法是:登录成功拿到一个JWT,塞进localStorage,之后每次请求带上Authorization: Bearer头。JWT的卖点是"无状态"——服务器验证签名就信任载荷,不用查数据库。问题是localStorage对页面上所有JavaScript公开,你的代码、第三方分析脚本、某个npm依赖包,谁都能读。一旦发生XSS攻击,一行fetch就能把token发到攻击者服务器,对方拿着它可以在任何地方冒充你,直到token过期。

常有人说"反正XSS一来就全完了,藏哪都一样"。这话只对了一半:XSS确实能让攻击者在页面里为所欲为,但token能不能被读走,决定了这次入侵是"一次性带走的钥匙"还是"只能在受害者浏览器里、被服务器风控和日志盯着的临时作案"。前者攻击者关掉标签页照样用你的身份操作到天荒地老,后者一旦用户关闭页面,攻击就结束了。

  • 结论.能不能读走token,决定攻击是永久生效还是仅限当场,这才是选型的真正分野。

教科书里的标准答案:三段式架构,代价是CSRF

安全社区公认的做法是把access token放进内存变量(刷新页面就丢,配合短生命周期),再用一个httpOnly cookie存放refresh token负责静默续期,JS完全碰不到它。这套组合把"被偷走"的风险降到很低。

但httpOnly cookie会被浏览器自动附带发送,这打开了CSRF(跨站请求伪造)的口子:一个恶意网页可以诱导你的浏览器悄悄向你的银行、你的后台发起带cookie的请求。防御手段并不神秘——CSRF token双重校验、SameSite属性、服务器校验Origin来源,三层叠加基本能堵住。但这里有个容易被忽略的细节:CSRF token本身必须放在JS能读到的普通cookie或页面里,不能也做成httpOnly,因为前端要把它复制进请求头去对账。不是所有敏感信息都能一藏了之。

标准三段式架构 Access Token 存内存变量 刷新页面即丢失 JS可读,但短命 Refresh Token httpOnly Cookie JS完全读不到 负责静默续期 CSRF Token 普通可读Cookie 前端复制进请求头 专门对付CSRF 三块分工不同,缺一环整套防线就漏一角

Auth0、Okta的默认值,其实没跟上这套教科书

这里出现了一个反差。Auth0官方支持文档写得很清楚,SPA SDK默认把token缓存在内存里,刷新页面就掉登录状态;如果开发者想要"刷新不掉线",常见做法是把缓存切成localStorage——这等于主动放弃了内存方案的安全边界。Okta在SPA场景把refresh token rotation设为默认,带重放检测,思路更进一步,但依然是"token交给前端管理"的模式,不是把主令牌锁进httpOnly cookie。真正把cookie-first做成默认架构、并当作官方安全卖点的,只有ClerkSupabase Auth的默认会话模型也是客户端可读的access/refresh token,想要httpOnly cookie得自己搭服务端组件。

服务商默认存储方式判断
Auth0内存,跨刷新常改用localStorage开发体验优先,安全边界随之松动
OktaToken交前端管理+刷新轮换强化了续期防重放,未锁死主令牌
ClerkhttpOnly cookie为核心少数把cookie-first做成默认的厂商
SupabaselocalStorage类客户端可读存储要cookie方案需自建服务端组件
教科书的最佳实践和厂商给你的默认配置,不是一回事

这不是这几家产品不专业,而是SPA天生要跨域调用API、要在不同前端框架里通用,httpOnly cookie在跨域场景下配置成本更高,厂商为了通用性和接入速度,往往把安全参数的调节权交还给开发者。

  • 风险.不少团队接入Auth0、Okta之后从不检查token存储配置,默认值就是终版方案,这恰好是攻击者最爱的那种"教科书都写了但没人做"的缺口。

对已经在用localStorage存长效JWT的存量项目,最现实的第一步不是推翻重写,而是把token生命周期缩短、加上刷新轮换和撤销列表,把"偷回家的钥匙"变成"随时能作废的钥匙"。真要迁移到内存+cookie方案,改造成本集中在CSRF防御的三层落地上,值得先在新项目里跑通,再回头改老代码。