摘要

  • RFC 10017 按安全能力依次讨论完整 BFF、令牌中介后端和浏览器 OAuth 客户端;完整 BFF 将访问令牌、刷新令牌和机密客户端凭据全部留在服务端。
  • 这种架构能阻止浏览器侧窃取令牌,也能阻止恶意代码自行以该机密客户端兑换新令牌,却无法阻止同源恶意代码借用户的有效 Cookie 会话向 BFF 下达指令。
  • 管理层仍须明确应用源、脚本供应链、Cookie 与 CSRF、BFF 的目标主机/路径/方法、资源授权、高风险操作确认、会话撤销以及最终业务结果的证据责任。

一套内部管理门户通过了令牌安全审计。JavaScript 内存里没有访问令牌,Local Storage 与 IndexedDB 里也没有。刷新令牌存放在 BFF 的服务端保险库,浏览器只持有一个 HttpOnlySecure 的会话 Cookie。

随后,一个被污染的前端依赖调用批量导出接口。浏览器自动带上 Cookie;BFF 识别会话,取出用户的 OAuth 令牌,给出站请求加上凭据,再把指令交给数据服务。资源服务器看到的是正确客户端、有效令牌和允许的 scope,于是导出敏感数据。

这次事故没有窃取令牌。攻击者利用的是一条合法的执行通道。

RFC 10017 于 2026 年 8 月作为 IETF 最佳当前实践发布。它强烈推荐在企业、敏感及处理个人数据的应用中采用 BFF,但同时明确承认:只要恶意代码已经在应用执行环境中运行,客户端劫持就仍然存在。架构能够改变攻击者必须控制什么,却不能自动证明用户此刻想做什么。

威胁不止是复制一个字符串

浏览器 OAuth 常被简化为“如何不让 JavaScript 看见 token”。RFC 10017 的模型更完整。恶意 JavaScript 或 WebAssembly 一旦进入应用上下文,便与正常代码共享浏览器权限:读取页面可见数据、访问同源存储、操作同源窗口、改写函数与事件流程,并从获准的源发出请求。

RFC 将后续能力分成四类。第一类只在某一时刻偷走现有令牌。第二类持续驻留,等待每次更新后再偷。第三类完全绕过原有存储,在隐藏窗口或 iframe 中启动新的 Authorization Code 流程,利用用户在 Authorization Server 上仍有效的会话取得一套独立令牌。第四类根本不搬走凭据,而是在当前浏览器中直接发请求。

这四种能力决定了安全控制的上限。短时访问令牌会缩小一次窃取的窗口,却拦不住持续驻留的代码拿走下一个。刷新令牌轮换能发现某些重复使用,但攻击者若总是拿到最新版,并阻止正常客户端再次使用,就未必出现 reuse 信号。Web Worker 可以隔离已存令牌,却不一定阻止发起新流程。

PKCE 仍是必须的。它能证明兑换授权码的实例持有最初产生的 verifier,防止截获或注入授权码。可是一旦恶意代码已经位于登记的 redirect origin 内,PKCE 无法识别两段同源代码谁代表用户真实意图。

DPoP 与不可导出密钥也有清晰价值:单独复制令牌不再足以在外部环境使用。但同源恶意代码可以在浏览器纯客户端中以自有密钥发起新流程,也可能调用当前环境里的签名或请求能力。密钥不能被复制,不等于错误的代码不能使用它。

BFF 关闭三扇门,也接管第四扇门

完整 BFF 架构把服务端组件变成 OAuth 机密客户端。它使用 Authorization Code 与 PKCE,保管访问令牌和刷新令牌,把它们与 Cookie 会话关联,并代理所有到 Resource Server 的调用。浏览器端不接触 OAuth 令牌。

这确实堵住三条攻击路径。浏览器里没有令牌可供一次性或持续窃取;恶意前端也缺少 BFF 的机密凭据,无法自行以同一登记客户端兑换新授权码;HttpOnly 还让脚本读不到会话标识,减少把短暂客户端控制转化为可携带会话的机会。

剩下的路径却是 BFF 正常功能本身:应用必须能够要求 BFF 行动。恶意代码如果在正确的源中运行,也能提出同样的请求。Cookie 由浏览器附加,BFF 再把会话翻译成令牌。攻击者没有跨过令牌保管边界,而是沿着指令边界向前走。

因此,BFF 不能只是一个万能反向代理。RFC 10017 要求严格控制出站目的地。前端的每条 BFF 路由应映射到预先批准的资源主机、路径和方法;浏览器传入的 URL 或 path 不能随意决定令牌要被送往哪里。若 /bff/fetch?url= 可以代理任意目标,服务端的好保管就变成了带用户令牌的开放转发能力。

Cookie 同样有独立规则。RFC 要求 SecureHttpOnly,建议 SameSite=Strict、path /、不设置 Domain,并采用合适的 host 绑定前缀。可是 Cookie 会被浏览器自动携带,所以更改状态的 BFF 接口仍需 CSRF 防护。JavaScript 读不到 Cookie,不代表跨站指令无法触发 Cookie。

BFF 也集中承接了机密客户端、令牌仓库、会话转换和代理路由。它更适合做异常检测与速率限制,却也成为容量、区域一致性和故障恢复的关键依赖。

令牌中介保护刷新权,却重新暴露即时权

第二种架构是令牌中介后端。后端仍是机密客户端,保管刷新令牌,但会把访问令牌交给浏览器,让前端直接调用资源服务器。它省去所有响应都经过代理的成本,安全能力也正好停在它重新暴露的边界。

恶意代码拿不到刷新令牌,也无法独立完成新的机密客户端流程;但访问令牌仍可被读取。即使把访问令牌藏在更隔离的内存中,恶意代码仍可能借有效会话向中介索取最新版。攻击者不需要长期权限,也能在短窗口内制造不可逆结果。

发送者约束在这里还有责任分裂:后端取得令牌,浏览器使用令牌。RFC 10017 并未定义这种双方分工的 DPoP 模式。架构图上画一条“PoP”箭头,不能代替实际的密钥归属、proof 生成与验证链。RFC 因而建议:只有在系统要求确实无法采用完整 BFF 时,才考虑令牌中介。

浏览器客户端仍然是公开客户端

第三种架构把 OAuth 责任全部放在浏览器。写进前端包的共享 secret 无法保密,不能被 Authorization Server 当成客户端身份凭据。浏览器客户端必须使用 Authorization Code 与 PKCE、精确登记 redirect URI,并实行合适的 CSRF 防护。

若浏览器获得刷新令牌,Authorization Server 必须在每次使用时轮换它,或对它实施发送者约束;还须设置总有效期或闲置过期,且不能借轮换无限延长预设的绝对寿命。这些是严肃的底线,却不能消除同源代码已经被控制后的四类攻击。

“源”由 scheme、host 和 port 构成,是浏览器执行权的实际边界。RFC 10017 建议每个源只部署一个应用。两个组织上分离的产品如果共享同一源,就共享了浏览器眼中的一组能力。CORS、CSP 与 postMessage 也只有在源的责任清楚时才容易验证。

CORS 只决定跨源响应能否向浏览器代码暴露,不是服务器端授权,也无法区分允许源里的正常与恶意脚本。窗口间消息同样不能因为由浏览器送达就被信任;发送方和接收方都必须精确核对 origin。

Service Worker 无法成为源之上的永久裁判

把 OAuth 流程和令牌放进 Service Worker 看似能在浏览器里复制 BFF:worker 拥有隔离内存,并为请求添加令牌。RFC 10017 不推荐这种方案,因为恶意应用代码可以注销 worker,再打开一个不受其拦截的新浏览上下文,发起新的授权流程。

这里暴露的是权力层级。Service Worker 能保住现有令牌,却无法在控制应用源的代码之上保持不可撤销的裁判地位。Web Crypto 的不可导出密钥也只约束 API 导出;规范不保证底层文件一定加密或由 TPM 保管,更不会自动判断调用者意图。

证据必须从脚本一直跟到业务结果

可用的审计链应关联:应用源与前端版本、依赖与 CSP 指纹、隐私安全的会话哈希、CSRF 结果、BFF 路由到资源主机/路径/方法的映射、令牌 audience 与 scope 摘要、资源策略版本、对象当前状态、幂等键以及最终 commit。原始令牌、Cookie 与客户端 secret 不应写进日志。

负面测试也要逐层开展:能否读取现有令牌;能否持续取得轮换值;能否在隐藏上下文启动新流程;能否向中介索要新访问令牌;能否不见令牌而调用 BFF;能否改变代理目的地;响应丢失后能否重复不可逆动作。一个“OAuth 校验成功”的总绿灯无法说明这些问题。

Running-Code Primacy 要求只把权力赋予实际执行的规则。“采用 BFF”是符号,不是结果。Minimum Initial Specification 可以让 OAuth 的共同交换规则保持精确;最终业务指令仍应由承担后果的本地服务判断。现实层级不混淆,才能避免把安全的令牌误当成安全的行动。