最近读到 Neciu Dan 在 2026 年 7 月 4 日发布的文章 What's the best way to do authentication in modern applications。文章从一个常见问题开始:前端登录后拿到的认证令牌,究竟应该放在 localStorage、内存还是 Cookie?
它最有用的部分,是把 XSS 之后的两种结果分开了。localStorage 中的 Bearer Token 被读走后,攻击者可以拿到自己的机器上继续使用。换成 HttpOnly Cookie,恶意脚本仍能借当前浏览器调用接口,却带不走 Cookie。两种情况都很糟,只是持续时间和处置方式不同。
读完以后,我基本赞同作者的选择,但也觉得标题里的问题问得太大。Session ID、OAuth Access Token 和 Refresh Token 都是凭据,用途并不一样。把它们统称为“认证令牌”,很容易争论半天却没在谈同一件事。
文章是怎样推导的
作者先从教程中常见的 localStorage 写法谈起。它省事,刷新页面后还在,问题也很直接:同源 JavaScript 可以读取。页面发生 XSS、第三方脚本被攻破,或者依赖里混入恶意代码时,Bearer Token 很容易被传出去。
把 Access Token 留在内存会好一点,但好得有限。恶意脚本和业务代码运行在同一个 JavaScript 环境中,可以劫持 fetch,也可以直接替用户调用接口。页面刷新和多标签页还会把状态恢复变成另一个问题,最后往往又需要一枚长期凭据来换取新的 Access Token。
于是文章转向 HttpOnly; Secure; SameSite Cookie。JavaScript 读不到它,浏览器会自动带上它。对于只有一个前端和一个后端的应用,作者倾向于让 Cookie 保存不透明的 Session ID,真实会话放在服务端。遇到第三方 OAuth,则把 Access Token 和 Refresh Token 收到 BFF 后面,浏览器仍然只拿 Session Cookie。
这条路线有依据。OWASP 建议不要把 Session 标识放进 localStorage,因为 XSS 可以读取其中的数据。IETF 的《OAuth 2.0 for Browser-Based Applications》草案也强烈推荐处理业务、敏感信息或个人数据的浏览器应用采用 BFF。
我更在意的是凭据边界
原文把 BFF 的 Session Cookie 比作寄存牌,并说它“不是 Token”。这个比喻容易理解,却不够严谨。
只要服务端凭某个值认出用户,它就是认证凭据。攻击者复制了仍然有效的 Session Cookie,照样可能接管会话。HttpOnly 限制的是 JavaScript 读取 Cookie,让攻击者较难把会话带到别处。它不负责修复 XSS,Cookie 也不会因此失去 Bearer Credential 的性质。
所以我不会先问“用 localStorage 还是 Cookie”。我更想知道:浏览器是否必须看到 Access Token,凭据被盗后还能使用多久,服务端能否立即撤销,以及项目是否值得承担 Session Store 或 BFF 的成本。答案不同,存储位置自然不同。
放到实际项目里怎么选
普通 Web 应用里,浏览器只和自己的后端通信,我会优先使用服务端 Session。Cookie 中只放随机、无业务含义的 Session ID,设置 HttpOnly、Secure 和合适的 SameSite,不设置 Domain,条件允许时使用 __Host- 前缀。登录、权限提升或密码重置后换一个 Session ID,服务端随时可以让旧会话失效。
我不太愿意为了“无状态”把用户 ID、角色和较长有效期一起塞进 JWT。JWT 适合在服务之间传递短期、可独立验证的声明。到了用户会话这里,退出登录、封禁账号和权限变更常常要求立即生效,纯无状态 JWT 最后又会引入撤销列表或版本检查。绕了一圈,系统还是有状态。
浏览器需要接入独立的 OAuth Authorization Server,并调用多个 Resource Server 时,我会先评估 BFF。Access Token 和 Refresh Token 都留在服务端,前端不用处理刷新与 Token Rotation。代价是所有请求都要经过代理,BFF 必须限制可转发的主机、路径和方法,还要承担限流、扩容与故障恢复。IETF 草案也承认,这比纯浏览器客户端复杂得多。
如果项目确实无法部署 BFF,浏览器又必须直接访问 Resource Server,那就使用 Authorization Code + PKCE,让 Access Token 短期有效并限制权限。Refresh Token Rotation、重用检测和并发刷新合并需要一起做。“存在内存里”只回答了一个很小的问题。
Cookie 方案没有免除 CSRF
Cookie 会被浏览器自动附带,这是它好用的原因,也是 CSRF 风险的来源。SameSite 很重要,但不应把所有希望都压在一个属性上。
修改数据的接口不能使用 GET。根据部署方式,还要采用同步器 Token 或经过签名的 Double Submit Cookie,并校验 Origin、Sec-Fetch-Site 等请求来源信号。BFF 如果允许跨源调用,可以要求自定义请求头触发 CORS Preflight,只放行明确的 Origin。子域之间通常是 Same-Site,却不是 Same-Origin,这里很容易误判。
防住 CSRF 也没有防住 XSS。恶意脚本运行在应用自身 Origin 时,照样能读取页面数据并调用合法接口。输入输出编码、内容净化、严格的 CSP,以及减少不受控的第三方脚本,这些基础工作不能被 Token 存储方案替代。
关于原文最后提到的 DBSC
文章最后谈到 Infostealer。恶意软件运行在用户设备上,不受浏览器同源策略约束,可以直接窃取磁盘上的 Cookie。HttpOnly 管不到拥有本机权限的进程。
Device Bound Session Credentials(DBSC)把会话续期绑定到设备上的不可导出密钥。复制出去的短期 Cookie 很快会过期,攻击者没有设备密钥,无法继续续期。Chromium 官方的 2026 年第一季度安全更新确认,DBSC 已面向 Windows 上的 Chrome 用户进入 General Availability。
我暂时不会让普通 Web 应用依赖 DBSC。它需要浏览器和服务端共同支持,平台覆盖也不完整。原文说 Chrome 146 于 2026 年 4 月在 Windows 推出,Chrome 146 官方发布页给出的 Stable 日期却是 3 月 10 日。月份有出入,DBSC 已经落地这件事倒是可以确认。现阶段,它适合作为额外保护,代替不了正常的 Session 管理。
我的结论没有原文标题那么痛快。长期 Bearer Token 不该放进 localStorage;同源 Web 应用通常适合 HttpOnly 的服务端 Session;处理敏感数据的 OAuth 浏览器应用值得优先评估 BFF。再往下就没有统一答案了。设计认证时,我会先决定凭据是否必须进入浏览器、多久失效以及怎样撤销,最后才是选择存储 API。
参考资料
- What's the best way to do authentication in modern applications
- OAuth 2.0 for Browser-Based Applications
- RFC 9700: Best Current Practice for OAuth 2.0 Security
- OWASP HTML5 Security Cheat Sheet
- OWASP Session Management Cheat Sheet
- OWASP Cross-Site Request Forgery Prevention Cheat Sheet
- Chrome Security 2026 Q1 Update
- Chrome 146 Release Notes