修复付费游戏播放会话在边缘转发 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

- 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:
2026-10-05 17:53:51 +08:00
parent 054709db65
commit d40df89e2c
14 changed files with 562 additions and 10 deletions
+26
View File
@@ -88,6 +88,32 @@ http {
}
# 平台付费游戏播放会话入口:`/api/game-distribution/play-sessions/<token>/…` 是 sandbox iframe
# 的 src,包内相对资源沿同一前缀解析。它与通用 `/api` 同口径代理到 api-server,唯一差别是
# 清空 Cookie:播放会话不读账号凭证,而 api-server 播放网关对带平台 refresh Cookie 的请求返回 403。
# 前缀 location 必须写 `^~`:不加时正则 location `~ ^/api(?:/|$)` 会先命中,Cookie 又会被转发。
location ^~ /api/game-distribution/play-sessions/ {
default_type application/json;
client_max_body_size 210m;
limit_conn genarrative_api_conn 64;
limit_req zone=genarrative_api_rps burst=64 nodelay;
proxy_pass http://genarrative_api;
proxy_http_version 1.1;
proxy_buffering off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
add_header X-Accel-Buffering no always;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Request-Id $request_id;
proxy_set_header Cookie "";
}
location ~ ^/api(?:/|$) {
default_type application/json;
# 中文注释:创作接口会携带参考图 Data URL,游戏发行包 PUT 更大,Nginx 只负责放行到 api-server;
+8
View File
@@ -108,3 +108,11 @@ curl -sSI -H 'Accept-Encoding: br' \
- 隔离靠 iframe 沙箱而不是独立来源:游戏文档跑在 `sandbox="allow-scripts"` 的不透明来源里,读不到主站 Cookie、storage 与 DOM,离开页面即随 iframe 卸载。
- 审核通过时 `api-server` 按 gameId 派生同源路径 `/games/<gameId>/` 作为 `entryUrl` 写入公开投影,部署侧不再需要配置发行域名。换版本或下架只改变后端公开投影,边缘不需要改配置。
- 门禁:`npm run check:nginx-spa-routes` 校验三份模板的 SPA allowlist(含 `/games`、`/games/detail`、`/games/play`、`/games/mine`、`/games/publish`、`/pay`、`/profile/payment`)与收银台深链前缀路由 `location ~* "^/pay/[^/]+/?$"`(`/pay/<checkoutToken>` 只放行「前缀 + 恰好一个路径段」;只放行裸前缀会让真实收银台链接落到默认 location 变 404),脚本自带正/反用例。历史上的独立来源模板与专属门禁已随同源方案上线删除。
## 付费游戏播放会话前缀(`/api/game-distribution/play-sessions/`)
- 付费游戏的可玩入口是 `POST /api/game-distribution/games/<gameId>/play-session` 换到的 `/api/game-distribution/play-sessions/<token>/`,直接作为 sandbox iframe 的 `src`;包内相对资源沿同一前缀解析。该前缀落在 `/api/*` 上,而通用 `/api` location 必须转发 Cookie(`/api/auth/*` 依赖 refresh cookie),Cookie 一旦被转发到 `api-server` 播放网关就会命中它的 403 纵深防御,iframe 与包内每个资源都不可用。
- 因此三份常驻模板(`genarrative.conf`、`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` location 一致,只多一条 `proxy_set_header Cookie ""`。
- `^~` 不能省:不加时 nginx 会先命中正则 location `~ ^/api(?:/|$)`,Cookie 又被转发回去。前缀末尾的斜杠也不能省:创建会话的 `POST /api/game-distribution/play-sessions` 需要账号凭证,必须继续走通用 `/api` 并保留 Cookie。
- 本地 dev 由 `vite.config.ts` 里排在 `/api/game-distribution` 之前的同名前缀规则做同一件事(`proxyReq.removeHeader('cookie')`);`api-server` 播放网关的 403 纵深防御不放宽,只保证边缘/dev 转发时不带 Cookie。
- 门禁:`npm run check:nginx-spa-routes` 对三份模板断言该 `^~` 前缀 location 存在、清空 Cookie、代理头齐全且排在通用 `/api` location 之前;`npm run check:pingora-route-parity` 断言矩阵里的 `play_sessions_gateway` 用例(以及 Pingora 的 `RouteDecision::PlaySessionGateway`)指向独立的清 Cookie 转发,不会被合并回通用 `/api` 规则。
+32
View File
@@ -116,6 +116,38 @@ server {
}
# 平台付费游戏播放会话入口:`/api/game-distribution/play-sessions/<token>/…` 是 sandbox iframe
# 的 src,包内相对资源沿同一前缀解析。它与通用 `/api` 同口径代理到 api-server(大小上限、
# 限流、超时、维护判断都保持一致),唯一差别是清空 Cookie:播放会话不读账号凭证,而
# api-server 播放网关对带平台 refresh Cookie 的请求返回 403(纵深防御保留)。
# 前缀 location 必须写 `^~`:不加时正则 location `~ ^/api(?:/|$)` 会先命中,Cookie 又会被转发。
# 只匹配带尾斜杠的前缀,创建会话的 `POST /api/game-distribution/play-sessions` 仍走通用 `/api`。
location ^~ /api/game-distribution/play-sessions/ {
default_type application/json;
client_max_body_size 210m;
limit_conn genarrative_api_conn 64;
limit_req zone=genarrative_api_rps burst=64 nodelay;
if ($genarrative_maintenance) {
return 503 '{"ok":false,"error":{"code":"MAINTENANCE","message":"服务维护中"}}';
}
proxy_pass http://genarrative_api;
proxy_http_version 1.1;
proxy_buffering off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
add_header X-Accel-Buffering no always;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Request-Id $request_id;
proxy_set_header Cookie "";
}
# 临时兼容主站仍在使用的 /api/* HTTP facade;前端完成 SpacetimeDB SDK 迁移后删除。
location ~ ^/api(?:/|$) {
default_type application/json;
+32
View File
@@ -144,6 +144,38 @@ server {
include /etc/nginx/snippets/genarrative-host-extras.conf;
# 平台付费游戏播放会话入口:`/api/game-distribution/play-sessions/<token>/…` 是 sandbox iframe
# 的 src,包内相对资源沿同一前缀解析。它与通用 `/api` 同口径代理到 api-server(大小上限、
# 限流、超时、维护判断都保持一致),唯一差别是清空 Cookie:播放会话不读账号凭证,而
# api-server 播放网关对带平台 refresh Cookie 的请求返回 403(纵深防御保留)。
# 前缀 location 必须写 `^~`:不加时正则 location `~ ^/api(?:/|$)` 会先命中,Cookie 又会被转发。
# 只匹配带尾斜杠的前缀,创建会话的 `POST /api/game-distribution/play-sessions` 仍走通用 `/api`。
location ^~ /api/game-distribution/play-sessions/ {
default_type application/json;
client_max_body_size 210m;
limit_conn genarrative_api_conn 64;
limit_req zone=genarrative_api_rps burst=64 nodelay;
if ($genarrative_maintenance) {
return 503 '{"ok":false,"error":{"code":"MAINTENANCE","message":"服务维护中"}}';
}
proxy_pass http://genarrative_api;
proxy_http_version 1.1;
proxy_buffering off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
add_header X-Accel-Buffering no always;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Request-Id $request_id;
proxy_set_header Cookie "";
}
# 临时兼容主站仍在使用的 /api/* HTTP facade;前端完成 SpacetimeDB SDK 迁移后删除。
location ~ ^/api(?:/|$) {
default_type application/json;
@@ -366,6 +366,27 @@
},
"docs": ["/games/game_<32 位十六进制 id>/…", "平台同源发行入口"]
},
{
"id": "play_sessions_gateway",
"samplePath": "/api/game-distribution/play-sessions/token_1/index.html",
"expect": {
"kind": "play_session_gateway",
"protectionClass": "api"
},
"nginx": {
"production": [
"location ^~ /api/game-distribution/play-sessions/",
"proxy_set_header Cookie \"\";",
"proxy_pass http://genarrative_api;"
],
"development": [
"location ^~ /api/game-distribution/play-sessions/",
"proxy_set_header Cookie \"\";",
"proxy_pass http://genarrative_api;"
]
},
"docs": ["平台付费游戏播放会话入口", "/api/game-distribution/play-sessions/<token>/…"]
},
{
"id": "web_spa_case_trailing_slash",
"samplePath": "/PROJECT/",