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

4.7 KiB
Raw Blame History

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 之前)加一段:

// 只在「本次响应确实下发了 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/。