SlaunchX Platform · core.gateway

Gateway 技术介绍

Gateway 不是业务模块,也不是基础设施工具箱。它是 HTTP 请求进入门户 Controller 之前的准入运行时:把不可信输入冻成事实,裁决能不能进业务,给幂等写一个跨进程的「执行一次」身份,然后收口响应。

草案 · 对应仓库文档 docs/plans/20260817-093100-gateway-first-principles-architecture-design.md · 接受前不是规范 · 现网 stepCode 以合同测试为准

门要解决什么

平台有四扇门户(SYSTEM / TENANT / PARTNER / CONSUMER),每扇门同时走 WEB 和 API。请求在到达 UseCase 之前,必须先回答四个问题:

  1. 路上看到的头、path、body,哪些可以当成事实?
  2. 说话的人是谁,站在哪个工作空间,有没有资格做这个动作?
  3. 同一笔写如果重试、断线、换会话,还算不算同一笔?
  4. 业务返回或失败之后,签名、缓存、错误体怎么收口,才不会把半成品漏出去?

TLS 只保证公网路上没人看。nginx 终止 TLS 之后,内部 hop 和日志都是明文。 SCv2 负责敏感 body 的应用层加密,但它在 Gateway 门外:Gateway 不实现密码学,只认服务端写入的会话所有权。

因此 Gateway 只做四件事,多做一件都是越权:

Admit

冻快照、发 requestId、做与路由无关的准入和预算。

Decide

按启动期锁死的 WEB/API 拓扑跑引擎,成功才发布 AccessContext。

Execute

幂等写先持久认领 operationId,再进 Controller。

Finalize

签名、回写缓存、释放预算、渲染错误。不重新裁决身份。

在分层里的位置

平台是四层:Entry → Orchestration → Capability → Persistence。Gateway 横在 Entry 前面,不属于其中任何一层,也不属于 core.infrastructure

浏览器 / 合作方
        │  TLS
        ▼
      nginx          TLS 在这里结束
        │  HTTP
        ▼
   Secure Channel    WEB 敏感端点解密(门外 SDK)
        │
        ▼
     Gateway         Admit → Decide → Execute → Finalize
        │  只发布 AccessContext
        ▼
   门户 Controller → UseCase → M2 Capability

依赖方向是硬的:

五条原理

后面所有结构都从这里推出,不能反过来用包名解释原理。

ID 原理 直接推论
P1 客户端输入不可信 头、path、query、body、X-Request-Id 只在 ingress 读一次。其后回读 Servlet 是漏洞。
P2 业务只看见已裁决身份 管道里可以有草稿。成功后唯一发布面是 AccessContext。门户不得读 GatewayContext 或 Redis 地址。
P3 「一次」是持久权威 Redis 幂等缓存是加速和回放约束,不是执行权威。声明幂等的写必须先认领 gio_*
P4 观察不决策 审计、指标、追踪不能改 Allow/Reject。强制观察失败只能收成 Fault。
P5 拓扑在启动期锁死 WEB/API 的 stepCode/order 是安全合同。加一个 @Component 不得改变决策。

现网大部分已经用约定测试锁住(进口面、禁 getHeader、门户禁 Holder、内部 DAG)。还没闭合的是:属性袋仍在(I5),认领切面还住在 config/(I8 的所有权)。

四个阶段

请求只有四段。包必须服从阶段,阶段不服从历史包名。

Admit

冻事实

HandlerMapping 之前。发出 canonical requestId,解析可信代理,检查 URI/体积,给会缓冲的路由领 buffer,把 Servlet 冻成 GatewayNormalizedInput。channel 此时必须是 UNRESOLVED

Decide

裁决

Interceptor 读 @GatewayValidation,按 WEB 13 / API 12 步跑引擎。每请求只发一次 PipelineTerminal(Allow / Reject / Fault)。Allow 才由 context-init 发布 AccessContext

Execute

执行一次

只对 @GatewayIdempotent。MFA 切面在前。先持久认领稳定 operationId,再进 Controller。切面不开覆盖业务的事务。

Finalize

收口

不重新解析 path,不重新裁决身份。API 签名失败会丢掉缓冲业务体,投影完整 503,中止幂等预约,绝不缓存未签名成功。

Secure Channel 插在阶段缝上:WEB 解密在冻明文快照之前,加密在 Finalize 之后。Gateway 只认 SecureChannelRequestAttributes.VERIFIED_SESSION_ID,不认客户端头。

一条 WEB 请求怎么走

以 SYSTEM 门户改密码为例。端点有 @GatewayValidation@SecureChannel、以及账号 MFA。

  1. nginx 把 HTTPS 打成 HTTP,带上 X-Request-Id / 转发头。
  2. 最外层生命周期 Filter 校验或生成 canonical requestId,写入 MDC 和响应头。非法客户端值在读 body 之前就拒绝。
  3. Admission:可信代理、URI、声明体积。这条路由会缓冲(Secure Channel + 可能的幂等),所以领一份 worst-case buffer 预算。
  4. SCv2 Filter 用已验证会话解密 body。Gateway 还没看见明文。
  5. Ingress 把明文头和 body 冻进 GatewayNormalizedInput。此后引擎禁止 getHeader
  6. Interceptor 从冻结 servletPath 认出 WEB,跑 13 步:完整性 → 版本 → 门户码 → 登录用户 → 限流 → workspace 要求 → 指纹 → workspace 快照 → 风控 → 幂等占位 → 人机证明 → 权限 → 发布 AccessContext
  7. 认证成功后,经 port 把 Secure Channel 会话从 __unbound__ 原子绑到用户。未绑定会话用绝对 TTL,不能靠滑动续期绕过全局容量。
  8. MFA 切面做 step-up。这不是 Decide 的一步。
  9. 若标记了幂等:Execute 认领 gio_*。已成功 / 进行中 / 结果未知 / 终态失败各有独立码,不进 Controller。
  10. Controller 只看见明文 Request 和 AccessContextHolder。改完后 SCv2 再加密响应。

一条 API 请求怎么走

CONSUMER API 用 API Key + 时间戳 + nonce + RSA 签名。没有指纹,没有 Turnstile。

  1. Admit 同样冻快照。API 一律领 buffer,因为出站要签名。
  2. Decide 12 步:完整性 → 版本 → 时间窗 → 锁出 → 验签认证 → 限流 → workspace 要求 → 快照 → 幂等 → 风控 → 权限 → 发布上下文。
  3. 锁出在验签前:已被锁的密钥不再做 RSA。时间窗在认证前:过期消息不值得验签。
  4. 凭证在 Redis 和日志里只以 SHA-256 假名出现。原始 key 不进桶键。
  5. Finalize 必须签出完整响应。签名失败则重置缓冲,投影 PLATFORM.CAPABILITY_UNAVAILABLE,不把半成品业务体和残缺签名头一起发出去。

事实台账

每个事实只回答四件事:在哪出生、谁可写、冻在哪、出管道后是否发布。

入站快照

GatewayNormalizedInput 在 Admit 结束后只读。Decide 唯一允许的突变是 withChannel(WEB|API)

事实 发布到 AccessContext
requestId / clientIp / 语言
method / path / query / servletPath / 原始 URI
分组头、API 签名材料、body 字节 否。仅白名单投影为 ClientRequestMetadata
apiKey / signature / nonce / Turnstile token / 门户访问码 永不发布
Cloudflare 国家 / 时区 仅经 ClientRequestMetadata

ClientRequestMetadata 只有 acceptLanguagecountrytimezone

四份草稿

用来取代今天 1051 行的 GatewayContext。引擎只写自己的那一份。

草稿 谁写 出管道
IdentityDraft 用户认证 / API Key 认证 operator* / session* / portal
ScopeDraft workspace / permission / 门户入口 workspace* / permissions / domain
ProtocolDraft 幂等 / 限流 / 人机证明 / 版本 只发布 idempotencyOperationId
RiskDraft 风控步 不发布,只进审计

交叉写入禁止:认证引擎写不了 ProtocolDraft;收口 Filter 读不到令牌邮箱;context-init 是唯一投影点。

失败矩阵

阶段决定失败语义。公共错误码由 presentation 从最终 PublicErrorResponseMetadata 投影,不从内部 stage code 直出。

Admit

直接拒绝,不跑引擎,不认领。只有 API、WEB 幂等、或有效 Secure Channel 合同才领 buffer。

条件 Stage code HTTP
客户端 Request-Id 非法 GATEWAY_REQUEST_ID_INVALID 400
可信代理失败 / 路由元数据不可用 GATEWAY_*_UNAVAILABLE / ROUTE_METADATA_UNAVAILABLE 500
URI / 体积 / IP 缺失 GATEWAY_URI_* / REQUEST_BODY_TOO_LARGE / CLIENT_IP_MISSING 400 / 413
buffer 预算耗尽 GATEWAY_BUFFER_MEMORY_CAPACITY_EXHAUSTED 503

Decide

Reject 是客户端或策略不成立。Fault 是实现或强制观察失败(缺快照、引擎崩溃、强制审计不可用)。Decide 失败不写 gio_*

Execute

认领结果 Stage code 进 Controller
CLAIMED
ALREADY_SUCCEEDED ..._ALREADY_SUCCEEDED 否,按稳定 ID 查询
IN_PROGRESS ..._IN_PROGRESS 否,不接管活执行
OUTCOME_UNKNOWN 且无可恢复标记 ..._OUTCOME_UNKNOWN
OUTCOME_UNKNOWN 且 @GatewayRecoverableOperation 恢复认领后是
FAILED_FINAL / REQUEST_MISMATCH 对应 409
journal 不可用 ..._JOURNAL_UNAVAILABLE 否,503

@GatewayRecoverableOperation 只能标在同时有 @GatewayIdempotent 的方法上,且只适用于「本地恢复意图 → Provider(带稳定 ID)→ 乐观锁收口」。启动期缺一不可。

管道序列(冻结)

序列是安全合同。改序等于改架构。这里冻结的是现网合同测试,并补上原理证明。2026-08-13 文档里的旧序作废。

WEB · 13 步

context-integrity
api-version-validation
portal-access-code
user-account-authentication
endpoint-rate-limit
workspace-requirement
fingerprint-validation
workspace-snapshot
risk-evaluation
idempotency-validation
human-proof-validation
permission-validation
context-initialization

API · 12 步

context-integrity
api-version-validation
timestamp-window
api-key-lockout
api-authentication
endpoint-rate-limit
workspace-requirement
workspace-snapshot
idempotency-validation
risk-evaluation
permission-validation
context-initialization

新增引擎必须同时改 Provider 拓扑、合同测试和架构文档。只加 @Component,启动失败。

身份模型

可执行通道只有 WEB 和 API。UNRESOLVED 只存在于 Admit。 规范运行时身份仍是 user + portal + workspace + workspaceKind

WEB API
主体 登录用户 + session + 指纹 API Key(operatorId = apiKeyBizId)
额外证明 门户码、指纹、Turnstile、workspace MFA 时间窗、nonce、RSA 签名
加密 敏感端点 SCv2 出站必须签名

门户和 M2 只读 AccessContext:请求标识、操作者、组织、权限、会话、clientIp、稳定 idempotencyOperationId、语言。NEW 态 Redis 缓存键、lease、原始 API Key 不在发布面。

端口

Gateway 内部不拥有这些实现。contract/ 定义 port,实现住在能力或 infra。

Port 实现归属
限流 / 幂等缓存 infra Redis。等待时间用 Redis TIME,不用 JVM 钟减
API 加解密与签名 developer_center 适配器,经 gateway-owned port
人机证明 / 工作空间 / 用户会话 / API Key 对应 M2 能力
持久认领 gio_* audit 能力实现;Gateway 只持 DurableOperationClaim
审计 sink / API 请求日志 强制失败变 Fault;日志异步,不挡请求线程
Secure Channel 绑定 support-secure-transport 适配

今天的违规:GatewayDurableOperationAspect 直接依赖 BusinessOperationClaimCapability。目标是切面只打 contract port,config/ 只做装配。

目标结构

时间轴四段。不要求立刻改名;改名是搬家,不是架构变更。

api/         唯一进口:注解、AccessContext、头投影、指纹
contract/    叶:头名 + port
error/       抛错词汇 + 公共错误映射
ingress/     Admit(含 buffer 预算)
pipeline/    Decide 编排 + 四份草稿
engine/      有序步骤,只读快照和自己的草稿
risk/        规则库,不回指 audit
audit/       旁路观察
execution/   Execute:认领切面(从 config/ 迁出)
egress/      Finalize:今日 http/ 的异常、收口、密文 Advice
config/      只装配

和现状的所有权差只有三处:

  1. http/ 同时待在 Admit 中段和 Finalize,所以后段还在读活 servletPath
  2. config/ 持有认领切面,把 Execute 藏进装配包。
  3. GatewayContext 按「又发现一个键」膨胀,应按生命周期拆草稿。

冻结面与接下来三场战役

冻结:五条原理、九条不变式、四阶段失败边界、事实台账、WEB 13 / API 12 序列、AccessContext 字段集、门户只准 import api/、SCv2 只认服务端 session。

细节(架构不变时可以改):某个引擎的 Lua、超时、order 数值、日志字段、类改名、单个错误文案。

  1. 事实源封闭:后段 Filter 改读快照,删除属性袋。
  2. 拆草稿GatewayContext 先变成门面委托,再删。
  3. 执行契约独立:认领 port 化,切面迁出 config/。搬家另开 PR。

单 PR 对着一个终点。不和全门户注解、业务状态机、连接管理混在一起。

文档状态

本文是架构草案的阅读版,不是规范。仓库里的权威草稿是 docs/plans/20260817-093100-gateway-first-principles-architecture-design.md。 接受后晋升为 docs/design/GATEWAY-ARCHITECTURE.md

现行包 DAG 和守卫清单仍看 docs/design/GATEWAY-PACKAGE-BOUNDARY.md。现网步序以 WebPipelineSequenceContractTest / ApiPipelineSequenceContractTest 为准。