mcpField
B
bff-entry-points

bff-entry-points

github.com/citypaul安全76061 次安装4 周前更新
代码仓库

关于 bff-entry-points

为设计后端前端层(BFF)入口安全而设的技能,面向实现浏览器与非浏览器调用边界的开发者。它强调显式访问分类,并通过预备好的注册器统一强制验证链、运行行为与 API 文档。

主要功能

  1. 01显式声明入口访问分类
  2. 02注册器统一安装验证链
  3. 03按访问类型推导处理器类型
  4. 04生成并校验入口目录与 OpenAPI
  5. 05涵盖实时连接及浏览器会话安全

适用场景

  1. 01为路由增加默认拒绝式访问控制
  2. 02保护浏览器状态变更请求免受 CSRF
  3. 03统一保护 SSE 与 WebSocket 入口
  4. 04通过自动化检查核对路由与文档

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/citypaul/.dotfiles --skill bff-entry-points

安装后在 Claude Code 中输入/bff-entry-points确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

agents/
openai.yaml
references/
browser-session-coordination.md
endpoint-protection.md
enforcement-and-testing.md
hexagonal-auth-boundaries.md
hono-example.md
realtime-entry-points.md

详细介绍

BFF 入口点

BFF 是一个驱动型宿主:它负责面向浏览器的 HTTP 边界,并代表一种用户体验调用应用操作。本技能负责定义该边界的行为安全模型——哪些人可以调用各个入口点、如何通过设计确保这一点,以及当调用方不是浏览器时,应用如何保持安全。它遵循 IETF 针对基于浏览器的应用给出的 BCP 轨道指南:令牌保留在服务端,浏览器仅持有会话 Cookie(draft-ietf-oauth-browser-based-apps §6.1——属于 BCP 轨道,在撰写本文时正处于 RFC Editor 流程中;RFC 9700 / BCP 240 是已发布的 OAuth 安全基线)。

加载 bff-design,了解本技能假定已确定的模式级决策——是否采用 BFF、采用几个、聚合与部分失败设计,以及如何将用户身份传递给上游服务。加载 structure-codebase,了解物理 endpoints/ 目录树、路由目录文件和仅供开发使用的路由;本技能定义每个端点叶节点必须声明的内容,以及注册流程如何强制执行这些要求。加载 hexagonal-architecture 了解端口和适配器;本技能进一步明确其规则:身份验证属于适配器工作,授权属于应用策略。加载 api-design 了解 REST 约定、错误正文格式(RFC 9457)、速率限制和版本控制。加载 secure-oauth-oidc 了解登录背后的 OAuth/OIDC 流程;本技能负责由此产生的应用会话。设计注册器的契约时,加载 codebase-design——这是一个刻意设计得很深入的模块。

实现前请阅读相关参考资料:

核心模型:显式访问分类

每个生产入口点都声明其访问分类。没有隐式默认值——未分类的路由会导致构建失败,而不是“由碰巧最先运行的中间件保护”(OWASP API5:2023 要求默认拒绝并显式授权;CWE-306 将缺少检查而非检查失效定义为漏洞)。

面向浏览器的 HTTP 端点至少应采用以下分类:

分类含义注册器安装的内容
public有意允许在没有应用会话的情况下调用验证、滥用防护、明确标记为无需认证的文档
protected-read已认证用户可使用的安全方法会话解析 → 401 → 应用内授权
protected-browser-mutation由浏览器发起的状态变更Origin/Fetch Metadata/内容类型策略 → 会话 → CSRF → 授权
// Canonical definition — references restate it only by pointer.
type EndpointAccess =
  | { readonly kind: 'public'; readonly justification: string }
  | { readonly kind: 'protected-read' }
  | { readonly kind: 'protected-browser-mutation' }
  | { readonly kind: 'protected-upgrade' };   // raw WebSocket upgrades — realtime reference

项目可以扩展这个可辨识联合类型——服务凭证、签名 Webhook、管理操作等——但每个入口点仍须进行明确、穷尽的声明,并且每种新增类型都要在注册器中定义自己的验证链。非浏览器驱动的适配器(CLI、作业运行器、队列消费者)也不能例外:每种适配器都须通过自身模式进行认证,并构造自己的调用方主体(参见六边形架构参考中的系统调用方部分)。

Public 描述的是调用方,而非信任程度。 公共端点仍须经过验证、限流和监控,并且不得包含机密信息——它并非仅限网络内部访问,也不免于滥用防护。健康探测是典型示例:对外公开,但只返回判断结果(不包含依赖项名称、版本或堆栈跟踪)。公共端点集合由应用决定;应将其保持精简,在经过审查的允许列表测试中对其进行快照,并将任何差异视为安全审查事项。

真相来源与派生目录

端点契约应保持在功能本地:路由、验证架构、访问声明、处理程序和文档都放在端点叶节点附近,让一次审查即可看到完整信息。就近放置并不意味着必须放在一个文件中;structure-codebase 负责安排重要的响应映射和工作流。

HTTP 边界会将应用层结果转换为安全响应。BFF 工作流可以协调应用调用与宿主负责的发布策略或生命周期策略,而应用授权、事实解析、领域决策和条件持久化仍由其现有负责人处理。异步发布时,HTTP 成功并不保证下游已收到消息。应保留现有的冲突处理和隐藏资源是否存在的语义,不得暴露其他调用方最终获胜的状态。

对于基于 Effect 的处理程序,请参阅运行时组合指南。

不要构建第二份由人工维护的集中式路由安全映射。它会逐渐偏离运行时注册情况,而且这种偏差要到发生事故时才会暴露。应根据以下两项既有事实派生集中式入口目录:

  1. 功能本地的端点契约;以及
  2. 显式的生产组合——包括实际挂载的契约,以及原始 WebSocket 升级。

注册器会记录每次挂载;目录是其输出,而非输入。强制执行测试会将目录与框架的路由表及生成的 OpenAPI 文档进行核对,从而自动实现集中可见性,而非依靠人工维护。

预备好的注册器(一个深模块)

组合时只需准备一次注册器,并提供端点绝不能自行选择的所有内容:会话解析、浏览器请求策略、安全的错误转换(RFC 9457 问题详情,不包含内部信息)以及遥测。端点所有者只需提供其契约和一个精简的处理器。

// composition/ — the only place that knows the enforcement machinery
const endpoints = createEndpointRegistrar({
  app,            // framework instance (Hono, Fastify, Fetch router...)
  sessions,       // cookie → validated session → AuthenticatedPrincipal
  browserPolicy,  // exact-origin allowlist, Fetch Metadata, CSRF, content types
  abuse,          // rate limits and payload caps, keyed by endpoint contract
  errors,         // safe problem-details translation
  telemetry,
});

// endpoints/api/orders/by-order-id/get.ts — contract + thin handler only
endpoints.register(getOrderContract, async ({ principal, params }) => {
  const result = await orders.viewOrder({ principal, orderId: params.orderId });
  return toHttpResponse(result);
});

端点所有者无法选择、排序或省略身份验证中间件——没有可选标志。处理器的类型根据声明派生(HandlerFor<Contract>,定义见端点保护参考):public 处理器不接收主体;受保护的处理器会接收一个它自己无法创建的 AuthenticatedPrincipal——通过模块边界和导入边界门禁强制确保其不可构造,而非依赖约定。对于公共端点,注册器会挂载处理器,并生成明确标记为未经身份验证的 API 文档(security: [],这是一个有意设置、便于搜索的标记,绝不能省略该字段)。对于受保护的端点,注册器会通过构造方式安装验证链,并生成匹配的 OpenAPI 安全元数据,因此运行时行为与文档不会相互矛盾。

从 codebase-design 的角度看,这是一个深模块:用小而稳定的契约(register)隐藏排序、标头策略、错误转换和文档生成。保持注册器核心与框架无关;只有内部适配器会接触 Hono/Fastify/Express API。

由此得出两条结论,而且每次变更都必须遵守,而不仅仅是在从零开始开发时。整条链只存在于注册器中,不得出现在其他地方:如果端点模块自行检查 Origin、CSRF 令牌或内容类型,就重新打开了注册器旨在封闭的接口——应将缺失的检查加入注册器的链中,并让端点接收检查结果。任何你修改的路由都必须在该次变更中经过注册器,包括已经直接挂载到应用上的路由:为它提供声明并迁移挂载位置。迁移挂载不会改变应用工厂的签名或其依赖项,因此,即使有指示要求保持这些不变,也不能以此为由保留绕过注册器的路径。**只有具备完整链的变更类型才能注册。**新增此类类型时,必须在注册器对应的分支中、处理器运行之前加入以下检查:精确的 Origin 允许列表匹配、Fetch Metadata 检查、内容类型检查、会话解析,以及与会话绑定的 CSRF 令牌检查。无论你如何命名该类型,都必须包含全部五项——一个解析会话并检查内容类型的分支只是一个接受请求正文的读取链,在该分支上注册变更操作,正是这项技能要防止的遗漏。在同一次变更中,构建该链所需但组合中尚未提供的任何内容:如果未配置允许列表,服务单个前端的服务应匹配请求自身的 Origin;如果会话记录没有令牌字段,则通过对会话 ID 执行 HMAC 来生成令牌(见下文步骤 3)。缺少协作组件应由本次变更补齐,绝不能成为减少检查项的理由。

请求顺序

对于受保护的读取:

  1. 解析并验证应用会话;
  2. 在缺少身份验证或身份验证无效时,返回稳定的 401;
  3. 在应用内授权所请求的产品资源或操作;
  4. 只有此后才能读取、订阅、签发凭据或执行其他受保护的工作。

对于受保护的浏览器变更操作:

  1. 拒绝无效的 Origin、不适当的 Fetch Metadata 或不受支持的内容类型——且不得泄露是否存在会话;
  2. 解析应用会话;
  3. 验证与会话绑定的 CSRF 保护——这不是可选步骤,也不能延后处理:如果会话记录无法携带同步器令牌,就使用服务器端密钥对会话 ID 进行 HMAC 运算,生成一个令牌(与会话绑定的签名双重提交令牌),并验证该令牌;不要仅凭 Origin 和内容类型校验链注册变更操作,再把令牌留待后续处理;
  4. 根据主体和已验证的参数,执行任何可进行的粗粒度操作授权或路径资源授权;
  5. 解析并验证有大小限制的请求正文;
  6. 授权正文中引用的每个对象、字段、目标和操作;
  7. 只有此后才能产生效果。

契约路径在所有位置(包括 HTTP 和升级契约)都使用 OpenAPI {param} 语法;注册器负责转换为框架语法,目录则存储契约形式。如果主要目标身份确实符合资源模型,就将其放在路径中;但不要仅为了避免依赖请求正文的授权,而扭曲批量、共享或字段级操作。延迟执行的 body() thunk 可以确保解析前先进行粗粒度路径检查;但所有有大小限制的请求正文数据仍必须在产生效果前完成验证和授权。

稳定的失败语义——每种情况都返回相同的外部可见响应,走相同的代码路径(不得采用针对具体原因的捷径),绝不充当信息预言机:

状态含义
401需要身份验证,或身份验证已失效
403明确披露的请求策略违规或已知的禁止访问结果
404无法访问或跨租户资源,且不得披露其是否存在(RFC 9110 §15.5.4 明确允许这种做法)
415不受支持的内容类型

API、SSE 和 WebSocket 入口会返回协议级失败。它们绝不会重定向到 HTML 登录页面——重定向后的 fetch 会得到无法解析的 200 HTML,而重定向后的 EventSource 则会因内容类型不符而以不透明方式失败。

六边形架构中的职责划分

BFF 是一个驱动适配器。它负责 Cookie 和请求头、会话解析、Origin/Fetch Metadata/CSRF/内容类型策略、HTTP 和升级协议响应、将已认证会话转换为不含提供方信息的主体,以及调用应用操作:

type AuthenticatedPrincipal = {
  readonly userId: UserId;      // branded domain types, not provider IDs
  readonly tenantId: TenantId;
};

BFF 不负责产品授权。每个受保护的应用操作都会在执行受保护的副作用之前,独立对主体进行授权——这一前提无论调用方是 HTTP、CLI、测试框架、另一个 BFF,还是未来的驱动适配器,都成立(OWASP ASVS 5.0 §8.3.1:在受信任的服务层强制执行)。内部代码绝不依赖 Hono、Cookie、Keycloak、OAuth 令牌、提供方组、HTTP 状态码或浏览器字段。

身份验证回答“谁在调用?”——这是适配器的工作。授权回答“此调用方可以执行这项产品操作吗?”——这是以产品语言表达的应用策略(例如 viewOrder 拒绝访问其他租户的订单),绝不能放进通用授权工具集合。应在规则所在之处证明它:对应的测试直接使用其他租户的主体调用操作——不经过 HTTP、Cookie 或 registrar——并断言拒绝结果,且不会对 fake 产生任何影响。通过路由驱动的测试只能证明接线正确,不能证明规则正确;如果你添加或更改了授权规则,却只通过 HTTP 测试覆盖它,那么这项工作尚未完成,因为下一个驱动适配器不会经过该路由。只有在授权成功后,才将主体映射为可追溯的领域参与者。数据库行级安全是对漏检的最后一道防线,而不是权限来源。

为什么采用这种设计

比较了用于端点保护的设计:

设计结论
1. 使用全局路径前缀中间件,并为公开路径设置例外隐式且容易受顺序影响:保护取决于注册顺序和路径匹配,升级请求会绕过它,例外列表则是未经审查的公开允许列表。仅依赖中间件的保护在实际环境中曾被绕过(Next.js CVE-2025-29927;Clerk CVE-2026-41248;Traefik fail-open GHSA-4mr2-fg2p-w63c)。
2. 集中维护路由安全映射形成第二个真相来源,且会与运行时注册脱节;脱节不会显现,并会导致默认放行。
3. 可选的路由局部中间件设计上默认放行——漏洞就在于缺少检查(CWE-306);每个路由负责人都要重新决定顺序,遗漏一处也无法在审查中发现。
4. 强制要求功能局部声明 + 预备 registrar + 派生目录首选。 从设计上默认拒绝,可在最末端审查,通过派生集中查看,并可通过测试强制执行。

有意接受设计 4 的权衡:registrar 是必须自行测试的前置机制;随着新认证模式出现,分类模型必须保持完备(扩展联合类型,绝不添加布尔标志);目录派生需要同时依赖契约和生产组合,因此组合必须保持显式;规模很小的服务也可以合理地先采用设计 1,并配上枚举所有路由的审计测试——但应在第二种认证模式或第一个实时端点出现之前采用 registrar。

在棕地系统中采用时,应逐步收紧,而不是重写:(1)先添加路由枚举测试,并将所有当前未分类的路由列入显式例外列表——该列表只能缩短;(2)从第一天起,所有新端点都通过 registrar 挂载;(3)逐个迁移现有路由,优先处理权限最高的路由;(4)每迁移一条路由,就逐个为相应操作添加可直接调用且不依赖提供方的拒绝测试——如果无法编写此类测试,说明授权仅依赖中间件,该操作就是下一项修复任务;(5)将门槛收紧至零例外,然后才信任该目录。

反模式

  • 在生产代码中直接将路由挂载到框架应用上,绕过注册器——包括早于注册器存在的路由。无论出于什么原因修改此类路由,尤其是安全修复,都意味着要在同一次更改中将其迁移到注册器,并为其声明专属访问权限:你修改后仍让它绕过注册器,就等于主动选择保留这个绕过;原地修复其逻辑,也会让下一个错误继续无法分类。
  • 使用 requireAuth: false、skipCsrf,或任何允许端点所有者削弱自身安全链的布尔值。
  • 手动编辑受保护路径列表,或将路径前缀匹配作为授权边界。
  • 只在 HTTP 中间件中进行授权判断——非 HTTP 调用方会因此完全绕过授权。
  • 让 AuthenticatedPrincipal 携带提供方字段(令牌声明、Keycloak 组、Cookie 名称)进入应用。
  • 信任客户端提供的操作主体、用户 ID 或租户 ID,而不使用从会话派生的主体。
  • 跨租户资源返回 403——这会确认资源存在;应使用不会泄露信息的 404。
  • 将 API、SSE 或 WebSocket 入口重定向到登录页面。
  • 让提供方令牌或回调参数进入浏览器存储(OWASP 会话管理备忘单;设立 BFF 正是为了让浏览器只持有 Cookie)。
  • 让仅供开发使用的端点可通过生产组合访问(OWASP API9:2023)。

完成检查

  • 每个已挂载的生产入口点——包括原始升级,以及此更改涉及的、先前挂载在应用上的任何路由——是否都通过注册器进行了明确的访问分类?
  • 能否列出公共端点集合?是否有经过审查的白名单测试将其固定下来?
  • 是否存在不经过注册器就能挂载路由的方式?如果有,是否会导致门禁检查失败?
  • 受保护的处理程序是否会接收无法由其自身构造、且类型由声明确定的主体?
  • 是否有直接且不依赖提供方的测试证明,每项受保护操作都会在产生任何影响之前拒绝未获授权的主体?
  • 运行时行为、派生目录和 OpenAPI 安全元数据是否一致——并由测试验证,而非依赖规范执行?
  • 401、403、404 和 415 在整个接口中是否始终保持稳定、不会泄露信息的语义?
  • 浏览器是否恰好只有一个身份验证协调器?实时连接失败时,是否会触发一次去重后的当前用户探测,而不是立即退出登录?

评论

支持 Markdown