Skip to content

BFF 模式与网关层设计 ​

看在前面 ​

目录里已有 BFF 概念篇(BFF概念-是什么与解决什么,讲"是什么/解决什么")。这篇补工程侧:BFF 和 API 网关的职责怎么切、聚合裁剪怎么做、鉴权在哪层下沉。素材取自 star-site 网关层真实分层(star-site-platform-gateway)。网关代码级的安全实践另见支付回调的HMAC验签与幂等设计。

一、BFF 定位:为谁聚合 ​

BFF 的关键词是 "For Frontend"——它不是一个技术组件,是一个按"端"切分的聚合层。没有它,一个页面的数据要由前端发 3 个请求自己拼。三端(Web/iOS/小程序)各自适配一遍服务端的原始模型:

 没有 BFF:                              有 BFF:
 Web ──┬─> user-service                  Web ──────┐
 iOS ──┼─> health-service        =>      iOS ──────┼──> 首页 BFF ──┬─> user-service
 小程序 ┴─> finance-service               小程序 ────┘             ├─> health-service
 (三端各自拼装、各自适配)                                        └─> finance-service
                                          (一个请求拿整页数据,差异在服务端吸收)

三个正当理由(与旧篇"正确姿势"一致):多端差异(同一功能不同形态,在 BFF 吸收而不是三端各写一遍)。服务聚合(微服务拆细后给前端一个聚合点,屏蔽内部拓扑)。后端迁移的缓冲带(下游换库换服务,BFF 内部改,前端无感)。对应的反模式也来自旧篇:非必要,莫新增。端形态没实质差异时,多一层 BFF 只是多一层故障点和代码重复。

二、与 API 网关的职责分界 ​

两者常被混为一谈。分界线一句话:网关管"所有请求共性的横切面",BFF 管"某一端个性的业务编排"。网关是南北向(外→内)的门卫,BFF 是东西向(内→内)的导演:

 Web / App / 小程序
        |
        v
 +------------------------------------------------------------+
 |  API 网关(star-site-platform-gateway)                      |
 |  横切面:JWT 验签、免鉴权白名单、路由 /api/** → 服务、        |
 |          限流、灰度、统一日志与 trace                        |
 +------------------------------------------------------------+
        |  按前缀路由(/api/health/** → health-service ...)
        +------------------+------------------+
        v                  v                  v
  health-service     finance-service    ai-service(Python)
                                        ↑ 事实上的"智能聚合层":
                                          一个对话请求按需调用多个工具

star-site 网关的真实横切面代码(JwtGlobalFilter):

java
// star-site-platform-gateway/.../JwtGlobalFilter.java
/** 免 token 白名单(/api/payment/notify/** 为渠道服务器异步回调,防线是渠道验签) */
private static final String[] WHITELIST = {
        "/api/auth/**",
        "/api/payment/notify/**",
        "/actuator/**",
};

这张白名单本身就是职责分界的标本。网关知道"哪些路径不需要用户身份",但不知道"渠道回调要验 HMAC"。那是 payment-service 的领域知识(验签+幂等+状态机三件套在服务内完成)。

网关的路由面是"前缀 → 服务"的端无关映射(示意,与白名单代码同源):

前缀目标说明
/api/auth/**auth-service白名单:登录注册本就无身份
/api/health/**health-service网关验 JWT 后放行
/api/payment/notify/**payment-service白名单:渠道回调,防线是 HMAC 验签
/api/ai/**ai-service网关验 JWT + 服务内二次验签

注意每一行的"防线"都不一样——网关的职责是把请求送到正确的门。每扇门自己的锁由门内的人负责,这就是横切面与领域逻辑的分界在路由表上的投影。

维度API 网关BFF
面向全部流量(端无关)某一端/某一页面集
职责鉴权、限流、路由、协议转换、可观测聚合、裁剪、端差异适配、页面级缓存
知道业务吗不知道(规则驱动)知道(页面/场景驱动)
谁维护平台/基础设施前端团队或贴近业务的团队
典型实现Nginx / Spring Cloud Gateway / APISIXNode / GraphQL / 轻量服务

三、聚合与裁剪的工程要点 ​

聚合不是"串行调一圈",四个硬问题都要答:

  • 并行 fan-out:页面要的三个下游无依赖就必须并发(Java 侧 CompletableFuture.allOf)。串行聚合的 BFF 只会把延迟叠成串——聚合层的 RT 应约等于最慢下游,而不是全部下游之和;
  • 超时预算分层:网关总超时 > BFF 聚合超时 > 单下游超时。倒挂的结果是下游还在跑、网关已回 504,用户重试再打一波——雪崩放大器;
  • 部分失败降级:3 个下游挂 1 个,返回"可用版页面"(该卡片降级文案)而不是整页 500。哪些数据可缺、缺了长什么样,是产品决策,要提前写进代码;
  • 页面级缓存与请求合并:BFF 距用户请求最近。BFF 是做页面级短 TTL 缓存(首页骨架 30s)与请求合并(同屏多卡片去重同一下游调用)的天然位置。但缓存键要含用户维度,否则串数据;
  • 裁剪输出:BFF 对外 DTO 与服务内部实体严格隔离。内部模型重构不外泄给端,也避免把不该给端看的字段(成本价、内部状态)顺手序列化出去。

GraphQL 是 BFF 的"自助餐化"变体(端自选字段),适合端形态差异极大的团队。代价是查询复杂度治理(深度限制、N+1 检测)与缓存难度,端少时普通 REST 聚合更划算。

四、鉴权下沉:网关验签、身份透传、服务不碰 token ​

分层原则:token 的解析与校验收敛在网关一处,下游服务拿到的是"已验证的身份":

 客户端带 JWT ──> ① 网关验签(JwtGlobalFilter:签名/过期/白名单)
                  ② 通过后透传身份头(X-Member-Id 等声明)
                  ③ 服务信任内网来源,按 memberId 做业务级授权
                  ④ AI 链路再加一道:ai-service 二次验签 + ContextVar 隔离

两处必须说清的细节。其一,"信任内网头"的前提是网络隔离:服务端口不对外暴露。star-site 只有 gateway 映射端口,见Dockerfile与镜像分层的编排要点。跨出这一前提,任何能直连服务的调用方都能伪造身份头。

其二,敏感链路服务侧二次验签:ai-service 不直接信网关头,而是 parse_member_id 再验一次并把身份写进 ContextVar。工具调用全部以该身份执行。"模型无法伪造身份链"的完整设计见智能体的身份链与工具安全。这与支付回调的白名单逻辑同构:每个入口想清楚"我的唯一防线是什么",防线之外一律不信任。

五、演进路径:什么时候引入 BFF ​

架构不预支:BFF 是"痛了才加"的层。判断三问——端形态有实质差异吗(Web/App 的数据形态差异大到值得服务端分叉)?页面拼装延迟已经是主要矛盾吗(前端串行 3 请求 > 聚合后 1 请求的收益)?下游服务重构频繁需要缓冲带吗?三问全否,网关直连微服务就是终态:

 阶段一(单体/小服务数)      阶段二(微服务化)          阶段三(多端+页面复杂)
 Client ──> Nginx            Client ──> 网关 ──> N 服务   Client ──> 网关 ──> BFF(按端) ──> N 服务
            └─ 直连,无中间层             ↑ star-site 现状                 ↑ 差异化压力出现时再上

star-site 的诚实定位:当前是阶段二。网关 JwtGlobalFilter 验签 + /api/** 前缀路由直达各服务,端形态差异靠服务自己的 DTO 兼容。没有独立 BFF 层,也暂时不需要。

真正在"聚合"的是两处:Java 侧各服务内部按页面组织的聚合接口。另一处是 ai-service 的家庭助手——一个对话请求按需调用多个工具(健康数据 + 财务数据 + 日程)。ReAct 循环是"运行时才决定聚合形状的 BFF"。把"我们现在不需要、哪一天会需要、需要时怎么演进"讲清,比硬凑一层 BFF 的简历架构诚实得多。这也是全库的方法论:非必要,莫新增。

追问链 ​

面试官:有了网关为什么还要 BFF?网关也能做聚合啊。

能做和该做是两回事。把页面聚合逻辑塞进网关,等于把高频变的业务代码放进低频变的基础设施。网关发布要全流量的变更窗口,页面改版等不起。网挂了(逻辑越重越容易)影响面是全站。横切面归网关、业务编排归 BFF,变更节奏不同的东西物理隔离。

面试官:BFF 会不会成为单点/瓶颈?

会,如果没有纪律:一是性能上它是端到端 RT 的新增一跳,必须并行 fan-out + 超时预算控制。二是可用性上它无状态、可水平扩,但依赖扇出决定了它的故障半径。每个下游挂它都受影响,所以部分失败降级是必修课而不是优化项。三是组织上按端拆 BFF(Web-BFF/App-BFF)而不是一个大 BFF 服务所有人,否则它自己长成新单体。

面试官:鉴权放网关了,服务内部调用还要鉴权吗?

分层回答:内网调用至少要有网络层隔离(不暴露端口/VPC)。重要服务加服务间认证(mTLS 或内部签发的调用凭证)。零信任的口径是"位置不构成信任"——K8s 集群内 Service Mesh 的 sidecar mTLS 就是把这件事标准化。star-site 现状是内网隔离 + 敏感链路(ai-service)二次验签,属于务实的中间态。

面试官:BFF 和前端团队的分工边界?

常见组织形态是 BFF 由前端团队或"贴前端的全栈小队"维护。因为它的需求节奏跟页面走而不是跟领域服务走。服务团队维护领域 API 的稳定性与语义,BFF 团队维护页面契约。DTO 隔离让两侧可以独立演进,这正是它叫 Backend For Frontend 的原因。

面试官:按端拆了 Web-BFF / App-BFF 之后,重复代码怎么治?

三招递进:公共聚合逻辑下沉为"共享领域服务或 SDK 包"(两端 BFF 只做差异适配)。用 GraphQL 把"选字段"交还端,聚合代码只剩一份。最激进的按页面而非按端拆(同一页面多端共享一个 BFF 模块)。判断标准看变化方向——差异随"端"变就按端拆,差异随"页面"变就按页面拆。跟着变化轴切才能让每次改动只落一处。

面试官视角小结 ​

  1. BFF 定位一句话:按端切分的业务聚合层,为"页面数据整取、端差异吸收"存在。三个正当理由(多端/聚合/迁移缓冲)+ 一条红线(非必要莫新增);
  2. 与网关分界:网关=端无关的横切面(验签/路由/限流)。BFF=端个性的编排(聚合/裁剪/降级)。star-site 的白名单代码是标本——网关知道"谁免验",不知道"回调验 HMAC";
  3. 聚合四件事:并行 fan-out、三层超时预算不倒挂。部分失败降级(页面可用而非整页 500)、DTO 隔离裁剪;
  4. 鉴权下沉链:网关验签 → 身份头透传(前提:内网隔离)→ 敏感链路二次验签。敏感链路二次验签指 ai-service 的 ContextVar 身份链。每个入口明确"唯一防线是什么";
  5. BFF 三风险:RT 新增一跳、故障半径=依赖扇出、组织上长成新单体。按端拆、无状态、可降级是对策。

2026-10 新增(留白补齐)。