60页技术规范写明了原因,但有个架构模式能让令牌盗窃在结构上成为不可能。

应用安全。我知道你想翻白眼。但就像你妈逼你吃青菜——你知道重要,总有一天会感谢她。

打开网易新闻 查看精彩图片

而且不是你没努力过。你做了PKCE,读了博客,把访问令牌从本地存储挪到内存里。甚至可能还搞了刷新令牌轮换。

抱歉,不够。

访问令牌还在JavaScript内存里。页面上任何XSS漏洞、任何被攻破的间接npm依赖(这事真发生过)、任何注入的分析脚本、用户上周二装的浏览器扩展——这些全都能完整访问那个令牌。泄露一次,攻击者就拿到一个能从任何地方使用的凭证,直到过期。如果刷新令牌能从JS访问到,他们还能持久化。

这不是什么激进观点。IETF关于浏览器应用OAuth的工作草案BCP212里写着:

「恶意JavaScript代码拥有与合法应用代码相同的权限。」

这是规范作者的原话,不是我说的。

三种攻击路径,一个共同病根

BCP212的威胁模型值得全文阅读(我知道你不会),但核心可以归结为针对任何在JS中持有令牌的单页应用的三种具体攻击:

第一,令牌泄露。XSS脚本直接读取内存中的令牌,外传到攻击者服务器。这是最直接的路径。

第二,权限滥用。恶意代码不需要偷走令牌,直接在用户浏览器里用令牌调用API,以用户身份执行操作。转账、删数据、改密码——都在用户会话里完成。

第三,持久化控制。如果刷新令牌可达,攻击者建立长期访问,即使用户改密码、清缓存,直到刷新令牌本身过期或被吊销。

规范作者画了三种架构回应:令牌放JS里(明确标注为最弱)、Service Worker模式、BFF模式。只有BFF模式把令牌彻底移出浏览器。前两种缩小攻击面,BFF消灭资产本身。

BFF模式:令牌彻底消失

Cloudflare Worker端到端拥有OAuth流程。它处理重定向、授权码交换、刷新。访问令牌和刷新令牌存在Cloudflare KV里,用一个不透明会话ID做键。浏览器只拿到一样东西:HttpOnly、Secure、SameSite=Lax、带__Host-前缀的会话Cookie。没有JWT,没有令牌,没有声明。没有任何可读的东西。

应用调用/api/whatever时,Worker查会话、从KV取访问令牌、在服务端注入Authorization: Bearer …再代理请求。令牌刷新透明发生,客户端完全不知道。

在BFF应用上打开DevTools。你会看到一个Cookie。没什么可偷的,因为那里什么都没有。

还有个被讨论不够的好处:前端变简单了。没有OAuth库。没有令牌存储逻辑。没有刷新调度。没有useAuth钩子把令牌穿遍组件树。你的React应用调用API像2015年那样——fetch,拿到响应,完事。所有认证复杂度住在Worker里,它该在的地方。

公平质疑与边界澄清

合理的反对:XSS仍然可以通过会话Cookie发起请求。攻击者能在受害者浏览器里代理API调用,永远看不到令牌本身。BCP212承认这一点。

区别在于影响半径。没有BFF,攻击者偷走令牌后,能从任何位置、任何设备、任何网络环境持续调用API,直到令牌过期。有BFF,攻击被限制在用户的浏览器会话里——用户关闭标签页、清除Cookie、或者会话过期,攻击就停止。

这不是完美的安全,是风险结构的根本改变。从"全球持久访问"降级到"浏览器会话内的临时代理",差距巨大。

另一个常见顾虑:延迟。每次API调用都要走Worker查KV,会不会慢?

Cloudflare KV的边缘缓存让读取通常在几十毫秒内完成。对比令牌泄露后的灾难响应成本——强制全员重新登录、吊销令牌、审计日志、用户通知、可能的监管报告——这点延迟是廉价的保险。

迁移成本:三十分钟的真实估算

作者说"三十分钟工作,Claude辅助设置"。拆解一下这具体指什么:

如果你已经在用Cloudflare,Worker的搭建是分钟级。OAuth重定向逻辑从客户端移到Worker,核心是把原本在React里的useEffect里的代码交换,改成Worker的fetch处理器。

KV绑定需要创建命名空间、设置权限、部署。Claude这类工具能生成样板代码,但你需要理解每一步在做什么——否则调试时抓瞎。

前端剥离更直接。删除@auth0/auth0-react或类似库,删掉useAuth hook,把带认证的fetch换成普通fetch。Cookie会自动带上,这是浏览器行为。

真正的时间黑洞在测试:跨域场景、Cookie属性在各种浏览器的表现、移动端WebView的兼容、第三方登录回调的处理。三十分钟是理想路径,复杂存量应用可能需要一到两天。

但对比"下次XSS漏洞时的凌晨三点应急",这个投入是理性的。

为什么是现在

npm供应链攻击在2023-2024年显著增加。恶意包通过依赖树渗透,在构建时或运行时注入代码。传统的"我们代码没漏洞"假设已经破产——你依赖的依赖的依赖可能正在读取你的内存。

浏览器扩展生态是另一个盲区。用户安装的"优惠券助手"或"网页截图工具"往往要求广泛权限,而扩展能访问页面DOM和JS执行环境。这不是理论风险,是持续发生的现实。

BFF模式不解决所有问题,但它把最敏感的凭证从攻击者的直接触及范围移走。在纵深防御的框架里,这是关键的一层。

React生态的默认设置——客户端OAuth、内存令牌、复杂状态管理——是方便开发者的优化,不是安全优先的设计。当便利成为漏洞的载体,重新评估架构假设就是技术债的偿还。

规范已经写在那里。攻击路径已经被枚举。解决方案的代码量以百行计,不是万行。剩下的问题是:你的用户数据值得这三十分钟吗?