跳到正文

Backend for Frontend(BFF)模式:架构图与权衡

每种客户端体验一个后端:BFF 的职责、与 API 网关和 GraphQL 的区别、如何让 OAuth 令牌远离浏览器,以及常见陷阱。

· 创始人兼开发者

发布于

三行架构图。Web 应用、移动应用和合作伙伴系统各自调用自己的 BFF,每个 BFF 再调用一个包含共享 catalog、orders 和 profile 服务的框架。Web BFF 通过身份提供商登录,并把令牌保存在会话存储中。

你的移动团队正在做一个商品页。通用 API 返回几十个手机从不显示的字段,评价、库存和愿望清单还各需要再调用一次。这些端点的每次改动,都要等一个同时服务网站和三家合作伙伴的 API 团队。

Backend for Frontend(BFF)为每种客户端体验提供一个自己的小型服务端 API,由构建该客户端的团队负责。本文介绍这个模式的由来、BFF 应该做什么、它与 API 网关和 GraphQL 的比较、它如何让 OAuth 令牌远离浏览器、常见陷阱,以及如何画图。

模式的由来

给这个模式命名的是 SoundCloud。Phil Calçado 的2015 年文章描述了基于公开 API 构建的应用:个人主页会调用 /tracks/1234.json、/tracks/1234/related.json、/users/86762.json 和 /users/me.json,再自己合并结果。客户端团队拥有自己的后端之后,就把合并工作搬到了那里,应用只需请求 GET /user-profile/123.json。Web 技术负责人 Nick Fisher 创造了这个术语。Calçado 总结说:“BFF 是应用的一部分。”

Sam Newman 在 2015 年 11 月发表的文章 Backends For Frontends 把它推广开来:每种用户体验一个后端,由构建该 UI 的团队维护。

话痨客户端与一刀切的 API

  • 话痨客户端。 Newman 的例子是愿望清单:先获取清单,再获取其中各项的名称、价格和库存。这是两次相互依赖的往返;在移动网络上假设每次 150 ms,渲染开始前就要过去 300 ms。有了 BFF,手机只需一次往返,相互依赖的调用在数据中心内部执行,那里一跳最多只要几毫秒。
  • 按最大屏幕设计的载荷。 手机会下载从不显示的字段。
  • 共享的瓶颈。 每个客户端团队都要为同一个可部署单元排队提交改动,而它常常归另一个团队所有。

BFF 的职责

职责 放在 BFF 中 不放在 BFF 中
聚合 每个屏幕一个端点,并行调用各服务 每个客户端都需要的 join;这些属于服务
响应整形 选取、重命名和格式化字段 定价等业务规则
认证与会话 会话 Cookie、令牌的存储和刷新 最终授权;由服务决定
缓存 短时效的聚合响应 仅以 URL 作为键的缓存中的个性化数据
功能开关 该客户端看到哪个变体 会改变领域行为的开关

BFF、API 网关与 GraphQL 的比较

API 网关 BFF GraphQL 服务器
服务对象 所有客户端 一种客户端体验 通过一个 schema 服务任意客户端
通常归属 平台团队 客户端团队 图团队,或通过联邦由各领域团队负责
职责 TLS、路由、令牌检查、限流 聚合、整形、会话和令牌 让客户端选择字段;resolver 调用服务
屏幕变化时是否需要改动 否 是,在同一次发布中 通常只需改查询
典型故障 业务逻辑渗入 重复代码、扇出缓慢 昂贵的查询、N+1 的 resolver 调用、HTTP 缓存更难

常见的做法是在边缘放一个网关,后面再放 BFF,就像 API 网关架构图那样。一个共享的图可以取代按客户端划分的 BFF,Microsoft 的模式指南指出,如果 resolver 针对各前端定制,独立的 BFF 可能没有增加任何价值。代价是团队边界:每个客户端团队都要就同一个 schema 的改动进行协商。

归属与团队边界

BFF 属于客户端团队:Web 应用和它的 BFF 在同一个 pull request 中修改,并在同一次发布中上线。Newman 指出,移动团队甚至可以把逻辑挪进自己的 BFF,从而无需等待应用商店审核就能发布。

该有几个 BFF?Newman 引用了 Stewart Gleadow 的说法:一种体验,一个 BFF。REA Group 为 iOS 和 Android 分别运行 BFF;SoundCloud 的 iOS 和 Android 听众应用共用一个,其工程师告诉 Newman,他们可能会重新考虑。Pete Hodgson 的做法是沿着团队边界划分。你的工程组织架构图应该为每个 BFF 恰好标出一个负责团队。

合作伙伴 BFF 保持稳定、带版本的契约,并在服务变化时把它映射到各服务上,这样内部的改动就不会破坏合作伙伴的集成。把合作伙伴直接路由到带版本的服务 API 也行得通。

作为浏览器应用令牌处理器的 BFF

自己运行 OAuth 的单页应用会把令牌放在浏览器里,任何被注入的脚本都能窃取它们。JWT 指南列出了浏览器应用可以把令牌存放在哪里以及每种方式的风险。RFC 10017,即面向浏览器应用的 OAuth 2.0,在 2026 年 8 月成为 Best Current Practice(BCP 212)。它按安全性从高到低排列了三种架构:

模式 令牌所在位置 被注入的 JavaScript 能做什么 RFC 10017 的结论
BFF 只在服务器上 页面打开期间通过 BFF 发送请求 对业务和敏感应用强烈推荐
令牌中介后端 访问令牌在浏览器,刷新令牌在服务器 窃取访问令牌,并向后端索取新的 仅在完整 BFF 不可行时使用
基于浏览器的 OAuth 客户端 所有令牌都在浏览器 窃取每个令牌并运行新的流程 不推荐用于业务或敏感应用

这里的 BFF 是机密(confidential)OAuth 客户端。它运行带 PKCE 的 authorization code 流程,用自己的凭据交换授权码,把令牌与用户的会话绑定存储,并在转发的每个调用上附带访问令牌。浏览器只持有一个 Cookie;OAuth 2.0 流程图逐步讲解了 code 流程。

RFC 的主要规则:

  • Cookie: 必须设置 Secure 和 HttpOnly;推荐 SameSite=Strict、Path=/、不设 Domain,并使用 __Host-Http- 之类的名称前缀。Chrome 和 Firefox 会强制执行该前缀;Safari 在撰写本文时(2026 年 11 月)只应用其中的 __Host- 部分。
  • CSRF: 必须有防御措施。在每个 API 调用上加一个固定的自定义请求头,会对跨源请求强制触发 CORS 预检。
  • 代理: 只转发到允许列表中的主机。
  • 会话状态: 使用服务端存储(RFC 仅建议在小规模时采用),或把加密后的令牌放进 Cookie,大小限制在 RFC 6265 要求浏览器支持的每个 Cookie 4,096 字节之内。让会话的有效期与刷新令牌一致。
Set-Cookie: __Host-Http-sid=3f9c...; Secure; HttpOnly; SameSite=Strict; Path=/
// Same-origin fetch sends the cookie by default.
await fetch("/bff/orders", {
  method: "POST",
  headers: { "Content-Type": "application/json", "X-CSRF": "1" },
  body: JSON.stringify(order),
});

BFF 并不能阻止跨站脚本攻击:页面打开期间,被注入的代码仍然可以调用 BFF,所以要保留 Content Security Policy。原生应用改为遵循 RFC 8252,在系统浏览器中运行带 PKCE 的 code 流程,因此移动端 BFF 通常只需验证一个 bearer 令牌。

陷阱

  • 重复的逻辑。 Newman 宁可接受一些重复,也不愿用共享代码把各个 BFF 耦合在一起。当同一段逻辑出现第三次时就把它提取出来:技术代码做成库,业务逻辑做成领域服务。SoundCloud 在大约五个 BFF 各自拼装用户资料之后,构建了 UserProfileService。
  • 变成单体的 BFF。 警示信号:它拥有领域数据或业务规则、其他 BFF 调用它,或者多个团队向它提交代码。折扣由 Web BFF 计算而移动端 BFF 不计算,这样的 bug 会被客户发现。
  • 扇出延迟。 如果一个 BFF 并行调用五个服务,至少有一个服务的响应慢于其自身 p99 的概率是 1 − 0.99^5 ≈ 4.9%:BFF 的 p95 大致相当于各服务的 p99。Dean 和 Barroso 的 The Tail at Scale 介绍了应对办法。
  • 多一跳。 每个 BFF 都会增加延迟、一套部署和一个值班轮换,而按 IP 地址限流的服务现在只能看到 BFF 的地址。

超时、部分响应与断路器

为每个屏幕设定一个截止时间并拆分它:BFF 中有 800 ms 时,每个调用 300 ms 的超时可以为一次重试留出空间。并行运行相互独立的调用,并把截止时间往下传。

然后决定哪些数据是必需的。Newman 的愿望清单需要清单本身,但没有库存也可以渲染。返回你已有的数据,并标明缺失的部分:

{
  "items": [{ "id": "sku-123", "name": "Trail shoe", "price": "$89.00" }],
  "stock": null,
  "errors": [{ "section": "stock", "reason": "timeout" }]
}

给每个下游服务各配一个断路器和连接池,这样一个缓慢的服务会快速失败,而不是占住所有 worker;阈值和降级方案见断路器模式。只对幂等的读取重试,并且只在一层重试;聚合响应的缓存时间不要超过其中最新鲜部分所允许的时长。

BFF 架构图

三行架构图。Web 应用、移动应用和合作伙伴系统各自调用自己的 BFF,每个 BFF 再调用一个包含共享 catalog、orders 和 profile 服务的框架。Web BFF 通过身份提供商登录,并把令牌保存在会话存储中。

16 个组件 · 9 个连接

在 Archboard 中打开
图表内容
  • Shared services (domain teams)
  • Identity provider
  • Session store
  • Web app
  • Web BFF
  • Mobile app
  • Mobile BFF
  • Partner system
  • Partner BFF
  • Catalog service
  • Orders service
  • Profile service
  • session cookie only
  • bearer access token
  • client credentials token
  • parallel calls + token
  • parallel, fewer fields
  • stable v1 mapping
  • tokens by session ID
  • 1. log in (redirect)
  • 2. code + PKCE → tokens
三个客户端,三个 BFF。每个团队拥有自己的客户端和背后的 BFF,BFF 并行调用共享的领域服务。Web BFF 是 OAuth 客户端:浏览器只拿到会话 Cookie,令牌留在服务器上。

这张图的读法如下:

  • 每种客户端体验一行, 按负责团队着色。每个客户端只调用自己的 BFF。
  • 每个 BFF 向共享服务扇出; 它的箭头止于框架,因为它会调用屏幕所需的任意服务。
  • 虚线边只用于登录。 浏览器被重定向到身份提供商,然后 Web BFF 用授权码换取令牌,这一步与 OpenID Connect 登录相同。
  • 令牌留在服务器上, 以会话 ID 为键保存在会话存储中。加密 Cookie 也可行,超出小规模部署后 RFC 10017 更倾向于它。
  • Markdown 块列出了 Cookie 设置和一个按屏幕划分的预算示例。

把 BFF 图画清楚

  • 给每个客户端一行, 连同它的 BFF,并标注负责团队。
  • 标明每条边上传递的内容:会话 Cookie、bearer 令牌或客户端凭据。
  • 画出令牌所在的位置,是在存储中还是在加密 Cookie 中。
  • 把网关单独画出来。 网关和 BFF 共用一个方框会掩盖归属关系。
  • 留意枢纽。 一个连到所有客户端的 BFF 方框,就是通用后端。
  • 省略端点列表。 每个 BFF 画一条带标签的边指向服务边界就够了;服务一侧由另一张BFF 背后的微服务图来呈现。

在 Archboard 中构建

  1. 用 Google 或 GitHub 登录并创建一个图表。
  2. 从 C4 模型 包中,为 Web 应用和移动应用各添加一个 容器,为合作伙伴添加一个 外部系统,每行一个。
  3. 在每个客户端旁边为其 BFF 添加一个 容器,填写技术和描述。
  4. 从 基础 包添加一个名为 Shared services (domain teams) 的 框架,里面放三个 容器 节点:catalog、orders 和 profile。
  5. 在 Web 行上方,添加一个来自 安全 包的 身份提供商,以及一个来自 数据 包的 Redis 节点作为会话存储。
  6. 把每个客户端连到它的 BFF,把每个 BFF 连到框架,给边加上标签,把两条登录边设为虚线,并按团队给每一行着色。
  7. 添加一个基础包的 Markdown 块,写入 Cookie 设置和预算,并添加基础包的 文本 标签标注各团队。

如果你的服务已经画在 Mermaid 流程图里,Mermaid 转换器能给你一份可编辑的初稿。这个示例共用 13 个图表节点,在 Free 套餐的限制之内。

常见问题

什么是 Backend for Frontend(BFF)模式? 为某一种客户端体验(如 Web 应用、移动应用或合作伙伴集成)构建、并由该客户端团队负责的服务端 API。它聚合后端调用并整形响应。

BFF 和 API 网关有什么区别? 网关服务所有客户端,负责路由、令牌检查和限流。BFF 服务一个客户端,并随其屏幕变化。很多系统会在多个 BFF 前面放一个网关。

GraphQL 会取代 BFF 模式吗? 可以,因为客户端自己选择所需的字段。但仍然需要有人负责 schema 和查询限制,而为单个客户端构建的 GraphQL 服务器本身就是一个 BFF。

BFF 的认证是如何工作的? BFF 是机密 OAuth 客户端:它运行带 PKCE 的 authorization code 流程,把令牌保存在服务器上,只给浏览器一个 HttpOnly 会话 Cookie。

我应该有几个 BFF? 每种用户体验一个,并与团队保持一致。体验相同、负责团队也相同的两个应用可以共用一个。

免费开始 — 1 个席位,无需信用卡。

相关文章

← 返回博客

一分钟打开画布

使用 Google 或 GitHub 登录。单席位免费,团队加入后再升级。