Files
dsh_shenxian/dsh-server-docs/04-调整方案/98-治本-陈旧dsh-auth-cookie回写清理.md
T
admin 5ad755116e chore(docs): 文档库并入代码仓(R4 选 a)+ 索引/台账跟进
1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
   保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
   工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
   必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
   + ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
   ⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
   验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
2026-09-15 18:47:13 +08:00

79 lines
4.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 98-治本:陈旧 `dsh-auth-*` cookie 回写清理(2026-09-14 落地)
- 日期:2026-09-14 | 状态:✅ **已上线**(平台后端已重建重启,端到端实测通过)
- 触发:用户「**必须要治本**」(承档案 97 之后查明的 431 故障根因)
> **TL;DR**
> dsh **每次实例 (重)启动都换** `dsh-auth-<随机后缀>` 的 **cookie 名**;平台此前只在**转发给实例时**
> 丢弃旧的(`mergeCookieHeader`),**从不回写浏览器** ⇒ 浏览器 jar **只增不减** ⇒ `Cookie` 请求头
> 越涨越长 ⇒ 涨到 **~15 KB** 就被 Cloudflare / 本机 nginx 直接 **431** 拒掉 ⇒ **请求根本到不了平台**
> ⇒ 那条 11 MB 合并脚本取不到 ⇒ 客户端报 `bundle script … failed to load`(界面「Failed to load plugins」)。
> 修法:**在响应里对陈旧的名字回写 `Set-Cookie: …; Max-Age=0`** ⇒ 浏览器把它们丢掉 ⇒ jar 不再增长。
---
## 一、实测证据(本机对线上做的)
| `Cookie` 请求头大小 | 结果(直连 nginx:443) |
|---|---|
| 12,769 B | **401** ← 放行到平台 |
| **19,149 B** | **431** |
| 25,529 B | **431** |
⇒ 阈值落在 **13–19 KB**;⚠️ 19 KB 时**连 `/` 也 431** ⇒ 一过线就是"整站请求被拒",用户看到"立即报错、什么都没加载"。
**旁证(三条反常现象全对上)**:平台日志里**没有**那条 bundle 请求(请求被 CF/nginx 拒在门外)✓;
**无痕正常**(空 cookie 罐 ⇒ 头只有几百字节)✓;服务器侧一切正常(dsh 启动无错、无 OOM、产物语法通过)✓。
📌 **Cloudflare 官方 changelog(2025-10-16)**:此前「总 32 KB / 单头 16 KB」,之后提到 128 KB
⇒ 本轮 431 **更可能来自本机 nginx**(实测 19 KB 即 431),CF 只是把响应透传回来。
## 二、改了什么(只动 `proxy.ts` 一处)
在响应头块(重放 cookie 合并之后、`writeHead` 之前)加一段:
```ts
// 只在「本次响应确实下发了 dsh-auth-*」时才动手 —— 那时才确知当前有效的名字
const scList = Array.isArray(scNow) ? scNow : scNow === undefined ? [] : [String(scNow)]
const freshNames = scList.map(取名字).filter(Boolean)
if (freshNames.length > 0) {
const staleNames = (request.headers.cookie ?? '')
.split(';').map(取名字).filter((n) => n.startsWith('dsh-auth-') && !freshNames.includes(n))
if (staleNames.length > 0) {
headers['set-cookie'] = [...scList, ...staleNames.map((n) => `${n}=; Path=/; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT`)]
}
}
```
**为什么这样判据是安全的**:
- 只有"实例本次下发了 `dsh-auth-*`"(= 我们知道当前有效名字)时才清;
- 其余情形(普通 API 响应不带该 cookie)**什么都不做** —— 绝不误删正在用的那个;
- **不带 `Domain=`**(dsh 下发的是 host-only cookie,带 Domain 反而删不掉);
- 即使判断有误,档案 51 的「401 → 取新 cookie 透明重放」也会自愈。
`scripts/verify-inject.cjs` 增加防回退断言:「proxy.ts 会清理陈旧 dsh-auth cookie」。
## 三、验证
| 层 | 手段 | 结果 |
|---|---|---|
| 类型/构建/单测 | `npm run typecheck` + `npm run verify` + 服务器 `ci.sh` | ✅ EXIT=0 全绿;✅ CI OK(48 pass / 0 fail) |
| 断言 | `node scripts/verify-inject.cjs` | ✅ 新增项通过 |
| **端到端(最强)** | 临时 session(R4)→ `/api/dsh/enter` 取带 token 的 URL → **带 2 个假 `dsh-auth-*`** 请求它 | 303 响应含 **3 条 Set-Cookie**,其中 **2 条 `Max-Age=0`** 正是那两个假名字 ✅ |
## 四、⚠️ 它救不了"当下",只防复发
**已经 431 时,请求在 CF/nginx 就被拒了、到不了平台** ⇒ 平台没法回写清理指令。
⇒ **用户必须先手动清一次**(F12 → Application → Cookies → 只删名字以 `dsh-auth-` 开头的,**保留 `sid`**);
此后由本机制维持小 jar。
## 五、遗留 / 后续
- **根因仍存在**:dsh 官方每(重)启动换 cookie 名(我们不改官方)。本机制只是**跟着清**。
⇒ 推论:**频繁重启实例会持续制造新 cookie**(今天为部署重启十几次即为此例)——仍是"少重启"的又一个理由。
- 可选加固(未做):nginx 的 `large_client_header_buffers` 已是 `4 32k`,可不动;**CF 侧限额不可配**,
所以"保持小 jar"是唯一可靠路径。若日后仍偶发 431,可考虑在平台侧**直接把超过 N 个的 `dsh-auth-*`
从请求里剥掉**(转发时已有 `mergeCookieHeader` 丢弃逻辑,只是要加"超量"判据)。
- 回滚:`git revert <本次 commit>` → `npm run build` → `systemctl restart dshs`;
备份 `/opt/dsh/backups/pre-99-20260914-225704/`。