Gateway 技术介绍
Gateway 不是业务模块,也不是基础设施工具箱。它是 HTTP 请求进入门户 Controller 之前的准入运行时:把不可信输入冻成事实,裁决能不能进业务,给幂等写一个跨进程的「执行一次」身份,然后收口响应。
门要解决什么
平台有四扇门户(SYSTEM / TENANT / PARTNER / CONSUMER),每扇门同时走 WEB 和 API。请求在到达 UseCase 之前,必须先回答四个问题:
- 路上看到的头、path、body,哪些可以当成事实?
- 说话的人是谁,站在哪个工作空间,有没有资格做这个动作?
- 同一笔写如果重试、断线、换会话,还算不算同一笔?
- 业务返回或失败之后,签名、缓存、错误体怎么收口,才不会把半成品漏出去?
TLS 只保证公网路上没人看。nginx 终止 TLS 之后,内部 hop 和日志都是明文。 SCv2 负责敏感 body 的应用层加密,但它在 Gateway 门外:Gateway 不实现密码学,只认服务端写入的会话所有权。
因此 Gateway 只做四件事,多做一件都是越权:
冻快照、发 requestId、做与路由无关的准入和预算。
按启动期锁死的 WEB/API 拓扑跑引擎,成功才发布 AccessContext。
幂等写先持久认领 operationId,再进 Controller。
签名、回写缓存、释放预算、渲染错误。不重新裁决身份。
在分层里的位置
平台是四层: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
依赖方向是硬的:
- 门户只能 import
gateway.api - 引擎可以调用 M2(验工作空间、API Key、人机证明)——这是门的职责
-
gateway.api不能依赖 M2 实现,已发布类型不能依赖内部包 - infrastructure 不能反向依赖 gateway
五条原理
后面所有结构都从这里推出,不能反过来用包名解释原理。
| 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 的所有权)。
四个阶段
请求只有四段。包必须服从阶段,阶段不服从历史包名。
冻事实
HandlerMapping 之前。发出 canonical
requestId,解析可信代理,检查 URI/体积,给会缓冲的路由领
buffer,把 Servlet 冻成 GatewayNormalizedInput。channel
此时必须是 UNRESOLVED。
裁决
Interceptor 读 @GatewayValidation,按 WEB 13 / API 12
步跑引擎。每请求只发一次
PipelineTerminal(Allow / Reject / Fault)。Allow
才由 context-init 发布 AccessContext。
执行一次
只对 @GatewayIdempotent。MFA 切面在前。先持久认领稳定
operationId,再进 Controller。切面不开覆盖业务的事务。
收口
不重新解析 path,不重新裁决身份。API 签名失败会丢掉缓冲业务体,投影完整 503,中止幂等预约,绝不缓存未签名成功。
Secure Channel 插在阶段缝上:WEB 解密在冻明文快照之前,加密在 Finalize
之后。Gateway 只认
SecureChannelRequestAttributes.VERIFIED_SESSION_ID,不认客户端头。
一条 WEB 请求怎么走
以 SYSTEM 门户改密码为例。端点有
@GatewayValidation、@SecureChannel、以及账号 MFA。
-
nginx 把 HTTPS 打成 HTTP,带上
X-Request-Id/ 转发头。 -
最外层生命周期 Filter 校验或生成 canonical
requestId,写入 MDC 和响应头。非法客户端值在读 body 之前就拒绝。 - Admission:可信代理、URI、声明体积。这条路由会缓冲(Secure Channel + 可能的幂等),所以领一份 worst-case buffer 预算。
- SCv2 Filter 用已验证会话解密 body。Gateway 还没看见明文。
-
Ingress 把明文头和 body 冻进
GatewayNormalizedInput。此后引擎禁止getHeader。 -
Interceptor 从冻结
servletPath认出 WEB,跑 13 步:完整性 → 版本 → 门户码 → 登录用户 → 限流 → workspace 要求 → 指纹 → workspace 快照 → 风控 → 幂等占位 → 人机证明 → 权限 → 发布AccessContext。 -
认证成功后,经 port 把 Secure Channel 会话从
__unbound__原子绑到用户。未绑定会话用绝对 TTL,不能靠滑动续期绕过全局容量。 - MFA 切面做 step-up。这不是 Decide 的一步。
-
若标记了幂等:Execute 认领
gio_*。已成功 / 进行中 / 结果未知 / 终态失败各有独立码,不进 Controller。 -
Controller 只看见明文 Request 和
AccessContextHolder。改完后 SCv2 再加密响应。
一条 API 请求怎么走
CONSUMER API 用 API Key + 时间戳 + nonce + RSA 签名。没有指纹,没有 Turnstile。
- Admit 同样冻快照。API 一律领 buffer,因为出站要签名。
- Decide 12 步:完整性 → 版本 → 时间窗 → 锁出 → 验签认证 → 限流 → workspace 要求 → 快照 → 幂等 → 风控 → 权限 → 发布上下文。
- 锁出在验签前:已被锁的密钥不再做 RSA。时间窗在认证前:过期消息不值得验签。
- 凭证在 Redis 和日志里只以 SHA-256 假名出现。原始 key 不进桶键。
-
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 只有
acceptLanguage、country、timezone。
四份草稿
用来取代今天 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
- 限流在认证后:未认证只走 Admit 的 IP 面;端点桶按已认证主体计。
- 指纹绑定的是会话设备,不是成员资格,所以在 workspace 快照前。
- 风控需要范围事实,所以在快照后。
- 幂等在人机证明与权限前:回放约束自带证明策略;风险挑战不得从回放恢复。
- 权限在人机证明后:先证明是人,再授权动作。
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/ 只装配
和现状的所有权差只有三处:
-
http/同时待在 Admit 中段和 Finalize,所以后段还在读活servletPath。 config/持有认领切面,把 Execute 藏进装配包。-
GatewayContext按「又发现一个键」膨胀,应按生命周期拆草稿。
冻结面与接下来三场战役
冻结:五条原理、九条不变式、四阶段失败边界、事实台账、WEB 13 / API 12
序列、AccessContext 字段集、门户只准 import
api/、SCv2 只认服务端 session。
细节(架构不变时可以改):某个引擎的 Lua、超时、order
数值、日志字段、类改名、单个错误文案。
- 事实源封闭:后段 Filter 改读快照,删除属性袋。
-
拆草稿:
GatewayContext先变成门面委托,再删。 -
执行契约独立:认领 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 为准。