文档:同步客户端 IP 只信 X-Real-IP

- ADR:身份与去重键节写明 IP 取 nginx 覆盖的 X-Real-IP、XFF 只取末段,并追加 2026-10-04 修订记录
- 玩法链路:去重与限流节标注 IP 来源
- decision-log:2026-10-04 条目补安全修正说明与标题
This commit is contained in:
2026-10-04 11:39:00 +08:00
parent 95705cbb11
commit 08020e664b
3 changed files with 9 additions and 3 deletions
@@ -80,7 +80,9 @@
### 8. 身份与去重键
登录用 `userId`;匿名用前端 `localStorage` 持久随机 `clientId`(随请求体带上);两者都缺失时回退
`IP + UA`。30 分钟窗口按 `identity + gameId`。不新造匿名 cookie、不依赖登录后才有的设备指纹。
`IP + UA`。30 分钟窗口按 `identity + gameId`。不新造匿名 cookie、不依赖登录后才有的设备指纹。`IP`
取 nginx 覆盖写入的 `X-Real-IP`(无 CDN 时即真实 TCP 对端),不取可伪造的 `X-Forwarded-For` 首段;
限流键与微信支付下单的 `payer_client_ip` 同源(2026-10-04 补充)。
### 9. 落位 = 纯持久化 / 读模型
@@ -136,3 +138,6 @@ procedure 在 `spacetime-module`,facade + mapper 在 `spacetime-client`,缓
- 2026-10-04:关停不再强制 flush(原「正常 SIGTERM/滚动重启必须在 `finalize_shutdown` 内 force flush」
作废)。理由:关停时最后一个窗口丢失概率极低,而强制 flush 需要把 worker 生命周期接进关停顺序并为在途
网络写入等待;按"perf 与简单优先"取舍,直接放弃该窗口。同日明确 flush 任一分片失败即丢弃剩余分片。
- 2026-10-04:客户端 IP 解析改为优先 nginx 覆盖写入的 `X-Real-IP`,`X-Forwarded-For` 只作回退且取最后
一段(nginx 用 `$proxy_add_x_forwarded_for` 追加的真实对端),不再信任可伪造的首段——公开上报端点原来
用它做匿名身份与限流键,可被伪造 IP 绕过并灌水。无 CDN 前置时 `X-Real-IP` 即真实客户端。
@@ -1,11 +1,12 @@
# 决策记录
## 2026-10-04 游戏游玩次数修订:关停不强制 flush、flush 失败丢弃剩余分片
## 2026-10-04 游戏游玩次数修订:关停不强制 flush、flush 失败丢弃剩余分片、客户端 IP 只信 X-Real-IP
- 变更:ADR `docs/adr/【ADR】游戏游玩次数计数-2026-10-03.md` 修订——原「正常 SIGTERM/滚动重启必须在 `finalize_shutdown` 内 force flush」作废;崩溃、被杀、正常关停都允许丢最后一个未落库窗口,`api-server` 不再注册关停 flush。
- 新增:一次 flush 按 500 分片,任一分片失败即终止本次 flush,剩余分片直接丢弃(`Build` 只把当前分片放回下一轮),避免连接不通时每个分片各等一次连接超时把 worker 卡住。
- 理由:关停丢一个窗口概率极低,强制 flush 要为在途网络写入等待、并把 worker 生命周期接进关停顺序;按 perf 与简单优先取舍。
- 受影响实现:`game_play_counter_worker.rs`(删 `flush_game_play_counter_for_shutdown`、失败即 break)、`main.rs`(`finalize_shutdown` 去掉计数 flush)、`game_play_counter.rs`(`take_pending` 仅测试使用)、`modules/game_distribution.rs`(上报先做内存限流预检再查公开可见性)。
- 安全修正:`request_context::client_ip_from_headers` 改为优先 nginx 覆盖写入的 `X-Real-IP`,`X-Forwarded-For` 只作回退且取最后一段(nginx 用 `$proxy_add_x_forwarded_for` 追加的真实对端),不再信任可伪造的首段;公开上报端点的匿名身份/限流键与微信支付下单的 `payer_client_ip` 同时受益。无 CDN 前置时 `X-Real-IP` 即真实客户端。
## 2026-10-03 游戏游玩次数:api-server 内存去重缓冲 + 批量 procedure 落 play_count
@@ -142,7 +142,7 @@
- **触发点**:游玩页点击「开始游戏」时网页上报一次,不做 iframe load、不在发行网关计数、不设停留阈值;点击后即使 iframe 超时也计一次。计数失败静默,绝不阻断进入游戏。
- **落点**:复用 `game_distribution_game.play_count` 累计次数,随目录、详情、作者「我的游戏」与后台游戏管理投影读取,不另建计数表。
- **缓冲与延迟**:`api-server` 纯内存聚合(增量表 + 30 分钟去重表 + 限流表),flush 间隔由 `GENARRATIVE_GAME_PLAY_COUNTER_FLUSH_INTERVAL_MS` 配置(默认 5s),经批量 procedure 落库;读路径不叠加内存值,展示最多滞后一个 flush 间隔。崩溃、被杀和正常关停都允许丢最后一个未落库窗口,关停不做强制 flush。
- **去重与限流**:登录用 `userId`、匿名用网页持久的 `clientId`、都拿不到时回退 `IP + UA`;`identity + gameId` 30 分钟去重窗口,另按 `IP + gameId` 每分钟 60 次固定窗口限流(超限 429)。公开上报端点先做内存限流预检,超限直接 429、不再查公开可见性;非公开/下架/封禁返回 404 且不计数。
- **去重与限流**:登录用 `userId`、匿名用网页持久的 `clientId`、都拿不到时回退 `IP + UA`(`IP` 取 nginx 覆盖写入的 `X-Real-IP`,不取可伪造的 `X-Forwarded-For` 首段);`identity + gameId` 30 分钟去重窗口,另按 `IP + gameId` 每分钟 60 次固定窗口限流(超限 429)。公开上报端点先做内存限流预检,超限直接 429、不再查公开可见性;非公开/下架/封禁返回 404 且不计数。
- **写入语义**:批量 procedure 在事务内只对 `published` 且存在有效公开版本的记录做 `saturating_add`,且**不更新** `updated_at`(避免重排作者列表)。该指标定位为展示用次数,不做交易级幂等、双计补偿或跨实例窗口共享。flush 按 500 分片,任一分片失败即终止本次 flush、剩余分片直接丢弃:`Build`(确定未发出)只把当前分片放回下一轮,其余错误连本批一起丢弃。
### 发行路径、沙箱与网络能力