修复付费游戏播放会话在边缘转发 Cookie 导致 403 不可玩
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
- nginx:三份模板在通用 /api location 之前新增 location ^~ /api/game-distribution/play-sessions/,代理头与通用 /api 一致并清空 Cookie - nginx:^~ 保证该前缀不被正则 location ~ ^/api(?:/|$) 抢先;只匹配带尾斜杠的前缀,创建会话端点继续走通用 /api 并保留 Cookie - pingora:新增 RouteDecision::PlaySessionGateway,走 api 上游并同样套用 api 限流分组、大小上限与维护闸 - pingora:抽出 route_clears_cookie,发行入口 ReleaseGateway 与播放会话入口在上游代理阶段统一清空 Cookie - pingora:classify_path 在通用 /api 分支之前命中播放会话前缀,并新增播放会话路由、Cookie 清除与保护等级用例 - 路由矩阵:新增 play_sessions_gateway 用例,声明清空 Cookie 与 protectionClass api - 门禁 check:nginx-spa-routes:新增播放会话前缀断言——三份模板存在 ^~ location、块内清空 Cookie、代理头齐全且排在通用 /api location 之前 - 门禁 check:pingora-route-parity:新增断言——平台内容网关用例必须清空 Cookie 且不得复用通用 /api location,Rust 播放会话分支必须排在通用 /api 之前并由 route_clears_cookie 清理 - 门禁 check:pingora-gateway-smoke:新增真实网关用例——播放会话前缀转发到 api 上游并清空 Cookie、创建会话端点保留 Cookie - dev:vite.config.ts 在 /api/game-distribution 规则之前新增同名前缀代理并清除 Cookie - 文档:同步 Pingora 试点文档、本地开发运维文档、deploy/nginx/README 与 shared-memory 决策/踩坑记录
This commit is contained in:
@@ -1,5 +1,16 @@
|
||||
# 决策记录
|
||||
|
||||
## 2026-10-05 播放会话前缀在三处入口清空 Cookie,网关 403 纵深防御不变
|
||||
|
||||
- 背景:付费游戏的可玩入口是创建播放会话后拿到的 `/api/game-distribution/play-sessions/<token>/`(sandbox iframe 的 `src`,包内相对资源沿同一前缀解析)。该前缀落在 `/api/*` 上,边缘通用 `/api` location 必须转发 Cookie(`/api/auth/*` 需要 refresh cookie),而 `api-server` 播放网关对带可解析平台 refresh Cookie 的请求返回 403,导致真实浏览器里 iframe 与每个包内资源都 403、付费游戏实际不可玩。
|
||||
- 决策(边缘):三份 nginx 模板(`deploy/nginx/genarrative.conf`、`deploy/nginx/genarrative-dev-http.conf`、`deploy/container/nginx.conf`)在通用 `/api` location 之前内联 `location ^~ /api/game-distribution/play-sessions/`;代理头、`client_max_body_size 210m`、`limit_conn` / `limit_req`、超时与维护判断与通用 `/api` 保持一致,额外清空 Cookie。`^~` 必填(否则正则 location `~ ^/api(?:/|$)` 优先命中),且只匹配带尾斜杠的前缀。
|
||||
- 决策(dev):`vite.config.ts` 在 `/api/game-distribution` 之前加同名前缀代理规则,`proxyReq.removeHeader('cookie')`。
|
||||
- 决策(网关):`server-rs/crates/pingora-gateway` 新增 `RouteDecision::PlaySessionGateway`,与通用 `/api` 同口径(api 上游、api 限流分组、大小上限、维护闸),差别只在经新增的 `route_clears_cookie` 清空 Cookie(发行入口 `ReleaseGateway` 复用同一判定)。**不放宽** `api-server` 的 Cookie 拒绝。
|
||||
- 决策(边界):前缀只认带尾斜杠的形式,创建会话的 `POST /api/game-distribution/play-sessions` 与 `POST /api/game-distribution/games/{gameId}/play-session` 继续走通用 `/api` 并保留 Cookie。
|
||||
- 门禁:矩阵新增 `play_sessions_gateway` 用例;`check:nginx-spa-routes` 新增「该前缀 location 存在、清空 Cookie、排在通用 `/api` 之前」断言,`check:pingora-route-parity` 新增「矩阵用例必须声明清 Cookie 且不得复用通用 `/api` location / Rust 播放会话分支必须排在通用 `/api` 之前」断言,`check:pingora-gateway-smoke` 新增真实网关下「该前缀清 Cookie、创建会话端点保留 Cookie」用例。
|
||||
- 影响面:`deploy/nginx/{genarrative.conf,genarrative-dev-http.conf,README.md}`、`deploy/container/nginx.conf`、`vite.config.ts`、`server-rs/crates/pingora-gateway/src/main.rs`、`deploy/pingora/nginx-route-parity.matrix.json`、`scripts/check-{nginx-spa-routes,pingora-route-parity,pingora-gateway-smoke}.mjs`、`docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。
|
||||
- 关联:`docs/project-memory/shared-memory/pitfalls.md`「付费游戏播放会话前缀落在 `/api/*`」条。
|
||||
|
||||
## 2026-10-03 AGC 发布版本标签改为由工程内部版本派生,取代「用户可编辑标签」口径
|
||||
|
||||
- 背景:用户实机验收指出发布面板「项目版本」显示 v6,而 AGC 工程内部只有 4 条正式版本记录(资源总览「项目版本」栏目 4 张卡,顶栏「智能体修订」下拉同样只有这 4 条)。核实:面板值来自本地清单 `manifest.projectVersion` 这个可编辑标量,它被三条链路反复钉到**平台** `game_distribution_version.version_number` 上——发布成功回写(`apps/ai-game-creator-shell/src-tauri/src/game_distribution_publish.rs:1531-1535`)、打开面板回读绑定回填(`:536-541`)、用户手改(`:944-965`);而 `manifest.versions` 从头到尾不参与该值。`publicationRevision` 只做 CAS,与任何版本号都无推导关系(`module-game-distribution/src/domain.rs:27-40` 的版本号解析只比 `max_existing` 与 `requested`)。
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
|
||||
这里只记录对当前开发仍有用的症状、根因、排查方法和风险边界。同一事实保留一个当前口径;退役对象的专属过程与单轮测试结果由 Git 历史追溯。遇到旧路径或版本时,以现行代码和专题文档为准。
|
||||
|
||||
## 2026-10-05 付费游戏播放会话前缀落在 `/api/*`:边缘转发 Cookie 会让 iframe 与包内每个资源都 403
|
||||
|
||||
- **现象**:付费游戏在真实浏览器里打不开——播放会话 `src`(`/api/game-distribution/play-sessions/<token>/`)本身和包内每个相对资源(JS/CSS/图片/音频)全是 403,同一份包的免费游戏 `/games/<gameId>/` 正常。
|
||||
- **原因**:播放会话前缀落在 `/api/*` 上,而通用 `/api` location 必须转发 Cookie(`/api/auth/*` 依赖 refresh cookie);`api-server` 播放网关对带**可解析平台 refresh Cookie** 的请求返回 403(与发行网关同族的纵深防御,本次不放宽),于是沙箱 iframe 的每个同前缀请求都带 Cookie、都被拒。dev 侧同样复现:`vite.config.ts` 原本只对 `/games/...` 清 Cookie,`/api/game-distribution` 规则会转发 Cookie。
|
||||
- **处理(现行口径)**:三份 nginx 模板(`deploy/nginx/genarrative.conf`、`deploy/nginx/genarrative-dev-http.conf`、`deploy/container/nginx.conf`)在通用 `/api` location **之前**加 `location ^~ /api/game-distribution/play-sessions/`——`^~` 不能省,否则正则 location `~ ^/api(?:/|$)` 优先命中、Cookie 又被转发;代理头 / `client_max_body_size 210m` / `limit_conn` / `limit_req` / 超时 / 维护判断都与通用 `/api` 一致,只多一条 `proxy_set_header Cookie ""`。`vite.config.ts` 在 `/api/game-distribution` 之前加同名前缀规则(`proxyReq.removeHeader('cookie')`)。Pingora 侧对应 `RouteDecision::PlaySessionGateway`:路径原样走 api 上游,同样套 api 限流分组、大小上限与维护闸,差别只在新增的 `route_clears_cookie` 清空 Cookie(发行入口 `ReleaseGateway` 复用同一判定)。
|
||||
- **边界**:前缀**只匹配带尾斜杠**的形式——创建会话的 `POST /api/game-distribution/play-sessions`(以及 `POST /api/game-distribution/games/{gameId}/play-session`)需要账号凭证,必须继续走通用 `/api` 并保留 Cookie。网关的 403 拒绝保持不变,只在边缘/dev 保证请求不带 Cookie。
|
||||
- **门禁**:`npm run check:nginx-spa-routes` 对三份模板断言该 `^~` location 存在、块内清空 Cookie、代理头齐全且排在通用 `/api` location 之前(变异验证:删掉块内 `proxy_set_header Cookie "";` 立刻报「播放会话前缀 location 缺少代理片段」);`npm run check:pingora-route-parity` 断言矩阵 `play_sessions_gateway` 用例声明清 Cookie 片段、不复用通用 `/api` location,且 Rust `classify_path` 的播放会话分支排在通用 `/api` 之前(变异验证:把矩阵片段换成通用 location、或在 Rust 里交换两个分支,各自单独判红);`npm run check:pingora-gateway-smoke` 用真实网关二进制断言该前缀清 Cookie、创建会话端点保留 Cookie。三条都串在 `npm run lint` 链里,有自动调用方。
|
||||
- **关联**:`server-rs/crates/pingora-gateway/src/main.rs`、`deploy/pingora/nginx-route-parity.matrix.json`、`deploy/nginx/README.md`、`vite.config.ts`;另见本文件「主站 SPA allowlist 有三处真相源」条的「别踩」(发行入口不转发 Cookie 的同族规则)。
|
||||
|
||||
## 2026-10-03 AGC 随包 plugins 的 feature 档位必须与消费方一致,且门禁会因 build.rs 未重跑而假通过
|
||||
|
||||
- **现象**:Windows 本机 `npm run check:generated-bindings`(`npm run lint` 链内,`scripts/check-repository-ci.sh` 的 Repository checks 也走它)在 `build.rs:167:29` panic:`插件随包资源校验失败:随包插件存在未声明文件:.../src-tauri/resources/plugins/agc-godot-editor/native/gdextension/bin/win-x64/agc_godot_editor.dll(目标 x86_64-pc-windows-msvc 与当前 feature 组合不允许;请先执行随包资源准备步骤)`;树上换成 `agc-unity-editor/dotnet/publish/win-x64/Agc.Unity.Attach.exe` 时报同一类错。反向还有更隐蔽的形态:门禁 2 秒就 exit 0 说「通过」,但 tree 上其实带着编辑器产物。
|
||||
|
||||
@@ -532,6 +532,7 @@ dev 根盘空间在安装后曾接近满盘;2026-06-17 进入 canary 前已清
|
||||
| `/admin/*` | 先读取静态文件或目录 index,失败回退 `/admin/index.html`,HTML 默认 `no-cache`,并支持条件请求返回 `304` 与单段 `Range: bytes=` 返回 `206` / 越界返回 `416`。 |
|
||||
| `/assets/*` | 从 Web 根目录精确读取静态文件;带 Vite 指纹的文件默认长期缓存,其它文件默认 `no-cache`,并支持条件请求返回 `304` 与单段 `Range: bytes=` 返回 `206` / 越界返回 `416`。 |
|
||||
| `/api`、`/api/*` | 转发到 `api-server`,按配置执行 `Content-Length` 与流式 body 累计上限检查。 |
|
||||
| `/api/game-distribution/play-sessions/*` | 付费游戏播放会话入口,转发到 `api-server`;与 `/api/*` 同样限流、大小上限与维护判断,额外清空 `Cookie`(详见下节)。 |
|
||||
| `/v1/database/{db}/subscribe`、`/v1/identity*` | 转发到 SpacetimeDB,保留 WebSocket Upgrade 头。 |
|
||||
| `/__genarrative_pingora/healthz` | 仅在携带 `X-Genarrative-Pingora-Probe` 且匹配配置 token 时返回 shadow JSON,否则 404。 |
|
||||
| `/v1/*`、`/generated-*`、`/healthz*`、`/readyz*` | 返回 404,保持生产公网不暴露口径。 |
|
||||
@@ -545,6 +546,8 @@ SPA allowlist 里属于游戏分发入口的深链(游戏目录 / 详情 / 游
|
||||
|
||||
**平台同源发行入口**(`/games/game_<32 位十六进制 id>/…`)与 SPA allowlist 是两条不同的路由:Nginx 用 `location ~ "^/games/(?<game_id>game_[0-9a-f]{32})(?<game_path>/.*)?$"` 把它代理到 api-server 的发行网关(`proxy_set_header Cookie ""` + `proxy_pass .../api/game-distribution/releases/$game_id$game_path`),Pingora 侧对应 `RouteDecision::ReleaseGateway`:走 api 上游,但把上游路径重写成 `/api/game-distribution/releases/<gameId><asset 路径>`(与 Nginx 的 `proxy_pass` 同口径,原来的 query 不再拼接)、清空 `Cookie`,并按 Nginx 该 location 的语义既不进 SPA fallback、也不套用 `limit_conn` / `limit_req` 分组、不受维护闸拦截。只认小写、固定 32 位十六进制 id;`/games/detail` 这类 SPA 深链与 `/games/game/...` 这类形状不符的路径都不会被吞进发行网关。该口径由矩阵的 `games_release_gateway` 用例(含 `upstreamPath` 期望值)与 `cargo test -p pingora-gateway matches_nginx_route_parity_matrix` 固定。
|
||||
|
||||
**平台付费游戏播放会话入口**(`/api/game-distribution/play-sessions/<token>/…`,sandbox iframe 的 `src`,包内相对资源沿同一前缀解析)与通用 `/api` 路由只差一件事:转发时必须清空 `Cookie`。api-server 播放网关对带可解析平台 refresh Cookie 的请求返回 403(纵深防御,保留),Cookie 一旦被边缘转发,iframe 与包内每个相对资源都会 403,付费游戏实际不可玩(2026-10-05 就是这样暴露的)。因此 Nginx 三份模板都加 `location ^~ /api/game-distribution/play-sessions/`(`^~` 不能省,否则正则 location `~ ^/api(?:/|$)` 优先命中、Cookie 又被转发),代理头、`client_max_body_size`、`limit_conn` / `limit_req`、超时与维护判断都与通用 `/api` location 一致,只是多了 `proxy_set_header Cookie ""`;dev 侧 `vite.config.ts` 用同名前缀规则在通用 `/api/game-distribution` 之前清 Cookie。Pingora 侧对应 `RouteDecision::PlaySessionGateway`:路径原样走 api 上游、同样套用 api 限流分组、大小上限与维护闸,唯一差别是经 `route_clears_cookie` 清空 `Cookie`。前缀只匹配带尾斜杠的形式,创建会话的 `POST /api/game-distribution/play-sessions` 仍走通用 `/api` 并保留 Cookie。该口径由矩阵的 `play_sessions_gateway` 用例、`cargo test -p pingora-gateway matches_nginx_route_parity_matrix` 与 `npm run check:nginx-spa-routes` 里的播放会话 Cookie 隔离断言固定。
|
||||
|
||||
维护模式下,公网 API-like 路由返回 JSON `503`;公网 Web 静态路由先读取 `GENARRATIVE_PINGORA_GATEWAY_MAINTENANCE_PAGE_FILE` 指向的 release 外运行态公告,缺失时回退 `GENARRATIVE_PINGORA_GATEWAY_WEB_ROOT/maintenance.html`,两者都不存在时返回纯文本 `503`。版本化默认页不得包含日期或具体时段,临时公告由 `maintenance-on.sh --page-file` 安装并在 `maintenance-off.sh` 时清理。IPv4 loopback / RFC1918 / link-local 和 IPv6 loopback / ULA / link-local 来源绕过整站维护闸,主站页面与静态资源、普通 API、后台页面与后台 API、SpacetimeDB 路由均按非维护状态继续处理;应用层登录、管理员鉴权和其它业务鉴权保持不变。Pingora 直连按 TCP peer 判定来源;仅当 peer 是 loopback 的同机 Nginx 时才接受 Nginx 强制覆盖的 `X-Real-IP`,绝不使用客户端可伪造的 `X-Forwarded-For` 做维护放行。该放行只绕过网关维护响应;若 `pause-after-stdb` 已停止 api-server,内网普通 API 和后台 API 仍不可用。
|
||||
代理失败时,API / SpacetimeDB 等代理路由返回统一 JSON 网关错误;本地静态路由仍保持对应 HTTP 错误状态。
|
||||
静态 `Range` 只支持单段 bytes range;多段 range 暂按完整文件返回,避免在正式替换前引入 multipart 响应面。`If-None-Match` / `If-Modified-Since` 优先于 `Range` 判定,命中时仍返回 `304`;`If-Range` 日期匹配时继续返回 `206`,日期旧于文件或弱 ETag 校验器时回完整 `200`;`206` / `304` / `416` 不做 gzip 压缩,避免 `Content-Range` 语义被响应体改写破坏。Gateway smoke 会用固定 `X-Request-Id` 对账静态 `304`、`405`、`206`、`416` 的 Pingora access log 行,确认本地响应状态也进入正式切换证据链。
|
||||
|
||||
@@ -745,6 +745,7 @@ Jenkins 按 web / api / Spacetime module / build / deploy / publish 拆分
|
||||
- 会话与隔离:边缘在转发前 `proxy_set_header Cookie ""`,发行网关自身也对带 `Cookie` 的请求返回 `403`;游戏文档跑在 iframe `sandbox="allow-scripts"` 的不透明来源里,读不到主站 Cookie、storage 与 DOM,离开页面即随 iframe 卸载整套游戏代码。
|
||||
- 响应头与缓存:`X-Content-Type-Options`、CORP(`cross-origin`)、无凭据 CORS、HTML CSP、内容类型白名单与 `Cache-Control: public, max-age=60, must-revalidate` 都由发行网关设置,边缘不覆盖。换版与下架只改变后端公开投影,因此**最迟 60 秒**内新请求不再拿到旧版本;已经下载到浏览器的脚本无法远程抹除,撤销能力以"停止继续分发"为准。
|
||||
- 审核动作:管理员只提交审核结论与公开修订号,`entryUrl` 由 `api-server` 按 gameId 派生**同源路径** `/games/{gameId}/` 写入公开投影;dev / release / 预览环境口径完全一致,入口不再由部署侧配置,历史数据里的绝对 URL 继续兼容。
|
||||
- 付费游戏播放会话:付费游戏不走 `/games/<gameId>/`(未购买访问 404),可玩入口是创建播放会话后拿到的 `/api/game-distribution/play-sessions/<token>/`,它直接作为 iframe `src`,包内相对资源沿同一前缀解析。该前缀落在 `/api/*` 上,而通用 `/api` location 必须转发 Cookie(`/api/auth/*` 依赖 refresh cookie),Cookie 一到 `api-server` 播放网关就会命中它的 `403` 纵深防御,iframe 与包内每个资源都不可用。因此三份 nginx 模板在通用 `/api` location **之前**内联 `location ^~ /api/game-distribution/play-sessions/`(`^~` 不能省,否则正则 `~ ^/api(?:/|$)` 优先命中),代理头、`client_max_body_size 210m`、`limit_conn` / `limit_req`、超时与维护判断都与通用 `/api` 一致,只多一条 `proxy_set_header Cookie ""`;`vite.config.ts` 里排在 `/api/game-distribution` 之前的同名前缀规则做同一件事(`proxyReq.removeHeader('cookie')`)。网关的 Cookie 拒绝不放宽,只是让边缘/dev 转发时不带 Cookie。
|
||||
- 门禁:
|
||||
|
||||
```bash
|
||||
|
||||
Reference in New Issue
Block a user