从「成功日志」里识别假成功:SUCCESS 不等于服务端落库

举报
云上复盘笔记 发表于 2026/11/14 02:02:13 2026/11/14
【摘要】 一次真实故障复盘:无头浏览器登录态迁移。文中所有数字都来自 2026-11-14 凌晨在本机(macOS + Chrome 157 + Node 22.22.2)真实跑出的结果,脚本与判据均可复现。文中不出现任何真实口令、密钥或 cookie 值,只讨论机制与统计量。 0. 结论先行自动化脚本打印 SUCCESS,只说明这段代码认为自己走完了,不说明目标系统把结果写进去了。把日志当成事实,是...

一次真实故障复盘:无头浏览器登录态迁移。文中所有数字都来自 2026-11-14 凌晨在本机(macOS + Chrome 157 + Node 22.22.2)真实跑出的结果,脚本与判据均可复现。文中不出现任何真实口令、密钥或 cookie 值,只讨论机制与统计量。

0. 结论先行

自动化脚本打印 SUCCESS,只说明这段代码认为自己走完了,不说明目标系统把结果写进去了。把日志当成事实,是这轮故障里代价最大的一个错误:一度把「票据没注入」误判成「票据已过期」,据此整轮停手,当天积分 0 分。

可用的判据只有一条链:动作 → 服务端状态 → 积分明细。中间任何一跳缺失,都要把结论降级为「未确认」,而不是「失败」或「成功」。

1. 事故现场:六个"看起来没问题"的信号

时刻(北京时间) 脚本/页面给出的信号 实际状态 误判类型
01:43 LOGIN_GATE FAIL → 推断「CAS 票据 24h 过期」 票据完好地躺在另一个 Chrome profile 里,只是没被注入 归因错误:把「缺数据」当成「数据失效」
01:47 注入 48 条导出 cookie,INJECT_OK 48 / FAIL 0 /grow 仍判未登录:导出文件只含 developer.huaweicloud.cn 与 .huaweicloud.cn 两域,缺 auth.huaweicloud.cn 局部成功 ≠ 全局成功
01:48 解密 168 条华为域 cookie,DECRYPTED 0 / BAD 168 不是数据损坏,是 PBKDF2 迭代次数用错 参数错误伪装成数据损坏
01:49 换成正确迭代次数,163/168 解密成功并注入 LOGIN_GATE OK nick=true numericPoints=true loginWall=false 正确
02:0x Star 脚本 STAR_SUCCESS,计数 267→268 服务端 today_status=todo, evaluator=external_check 动作成功 ≠ 判定完成(外部轮询延迟)
02:0x DevSpace ECHO_DOM hit=false OCR 从 xterm canvas 截图中读到命令回显 DOM 读不到 ≠ 没执行

六个信号里有五个如果不加核验就会导向错误动作:重跑灌水、整轮跳过、反复改脚本。

2. 机制:登录态由三层状态决定,日志不属于任何一层

定义三个可观测集合:

  • L1 本地票据:磁盘 cookie 库里的记录,和运行时 context.cookies() 的集合。记为 CdiskC_{disk}、CliveC_{live}。
  • L2 页面会话:访问 /grow 后服务端渲染出的昵称与积分数字。它是 CliveC_{live} 经过服务端校验后的投影,不是 CliveC_{live} 本身。
  • L3 业务判定:任务卡的 is_finish / today_status,以及积分明细里的入账行。这是唯一与收益直接相关的状态。

设脚本输出 SUCCESS 为事件 ss,服务端真正落库为事件 dd。我们要控制的量是假成功率

FPR=P(¬d∣s)FPR = P(\lnot d \mid s)

工程上无法把 FPRFPR 压到 0,因为 ss 是本地事件、dd 是远端事件。能做的是引入一个独立于写入路径的观测 oo(列表页、状态 API、积分明细),并要求 oo 与 dd 一致后才结算。本轮六个信号里,INJECT_OK 属于 ss,nick=true 属于 o1o_1,积分明细 +10 属于 o2o_2;只用 ss 就会翻车。

2.1 Chrome v10 cookie 的加密定义

macOS 上 Chrome 把 cookie 值以 v10 前缀存进 SQLite,解密链路是:

K=PBKDF2-HMAC-SHA1(P, "saltysalt", c, 16),C=AES-128-CBCK, IV=0x2016(PKCS7(V))K = \mathrm{PBKDF2\text{-}HMAC\text{-}SHA1}(P,\ \text{"saltysalt"},\ c,\ 16),\qquad C = \mathrm{AES\text{-}128\text{-}CBC}_{K,\ IV=0x20^{16}}(\mathrm{PKCS7}(V))

其中 PP 来自系统钥匙串条目 Chrome Safe Storage,cc 是迭代次数,macOS 为 1003,Linux/Windows 为 1。存储字节为 "v10" || C。

这里有个很好用的自检性质:用错误的 KK 解密时,PKCS#7 填充校验通过的概率约为 1/2561/256(最后一字节必须是 0x01,或 0x02 02、……近似 2−82^{-8})。168 条样本的期望"偶然通过"数约 168/256≈0.66168/256 \approx 0.66 条,实测 0 条。所以「全量报 bad decrypt」几乎必然是密钥/参数错,而不是 168 条数据同时坏了——这条先验能直接把排查方向从「数据」扳回「参数」。

2.2 Session cookie 的语义陷阱

expires_utc = 0 表示会话级 cookie,Chrome 启动时清除。经 CDP 注入时,正确做法是不传 expires 字段;某些封装会把 expires=-1 当成 1970 年前的绝对时间直接丢弃,表现为「注入返回成功、运行时却查不到」,和本次 INJECT_OK 48 但缺域是同一类假成功。另外 domain 带前导点(如 .huaweicloud.cn)表示包含子域,导出/注入时必须原样保留,否则同名的 host-only cookie 与域 cookie 会互相覆盖。

3. 可运行代码(脱敏版)

下面这段就是本机跑通的解密核心,密钥从钥匙串读取、不落盘、不打印:

const crypto = require('crypto');
const { readFileSync } = require('fs');

// macOS: 1003 次;Linux/Windows: 1 次。两个都试,靠 PKCS#7 自动筛掉错的
function candidateKeys(keyFile) {
  const pass = readFileSync(keyFile, 'utf8').trim();
  return [1003, 1].map(c => crypto.pbkdf2Sync(pass, 'saltysalt', c, 16, 'sha1'));
}

function decrypt(encryptedValue, keys) {
  const pre = encryptedValue.subarray(0, 3).toString('latin1');
  const body = /^v\d{2}$/.test(pre) ? encryptedValue.subarray(3) : encryptedValue;
  const iv = Buffer.alloc(16, 0x20);
  for (const key of keys) {
    try {
      const d = crypto.createDecipheriv('aes-128-cbc', key, iv);
      let out = Buffer.concat([d.update(body), d.final()]); // 错 key 会抛 bad decrypt
      const head = out.subarray(0, 32);
      let bad = 0;
      for (const b of head) if (b < 0x20 || b > 0x7e) bad++;
      if (bad > 3 && out.length > 32) out = out.subarray(32); // 部分版本前置 domain hash
      const s = out.toString('utf8');
      if (s && !s.includes('\uFFFD')) return s;
    } catch (_) { /* 填充不合法 = 这个 key 不对 */ }
  }
  return null;
}

注入侧的关键两行——只给真正有到期时间的项传 expires:

const o = { name, value, domain, path: path || '/', secure: !!secure, httpOnly: !!httpOnly };
if (Number.isFinite(exp) && exp > 0 && exp > Date.now() / 1000) o.expires = Math.floor(exp);
// exp <= 0(session)时什么都不传,千万不要写 expires: -1
await ctx.addCookies(rows);

三跳核验(写完后必做,不依赖脚本的自我报告):

// 跳 1:运行时确实有票据
const live = (await ctx.cookies()).filter(c => /SSOTGC|SSOJTC/.test(c.name));
// 跳 2:页面会话确实认这个票据(昵称 + 积分数字 + 无登录墙)
const t = await growPage.evaluate(() => document.body.innerText);
const pageOk = /昵称|账号名/.test(t) && /我的积分\s*\d+/.test(t) && !/登录\/注册查看您的积分/.test(t);
// 跳 3:积分明细里出现今天的行;这一步才是收益的判据

4. 本机实测数据

实验 A:PBKDF2 迭代次数对解密成功率的影响(样本:同一份只读备份里 168 条 %huaweicloud% 记录,两轮除了 cc 之外完全相同)

迭代次数 cc 样本数 解密成功 失败 失败表现
1 168 0 168 bad decrypt(PKCS#7 填充校验)
1003 168 163 5 空值 / 非 UTF-8 明文,主动丢弃

163 条里会话级(无到期时间)135 条、持久 28 条;过期项 0 条。这个 0 很关键:没有任何一条是因为"过期"被跳过的,与「票据 24 小时过期」的初始推断直接矛盾。

实验 B:注入域覆盖与登录判定的关系

阶段 注入条数 覆盖域 SSOTGC/SSOJTC 运行时条数 判闸结果
仅用户导出的浏览器 cookie 文件 48 developer.huaweicloud.cn、.huaweicloud.cn 0 / 0 FAIL
追加 auth 域解密票据 163 含 auth.huaweicloud.cn、.auth.huaweicloud.cn 1 / 1 OK

运行时 cookie 总数变化:273 → 284 → 306。注意第一阶段 INJECT_OK 48 是真实的,48 条确实都在 context.cookies() 里,但缺的域恰恰是判定所依赖的那个。

实验 C:三跳状态并不总是同步

动作 跳 1 本地 跳 2 服务端状态 跳 3 积分明细
签到 脚本无回显 — +10 @ 01:46 已入账
浏览鸿蒙案例 停留 35s status=done 待复检
Star 鸿蒙案例仓 计数 267→268 todo / external_check 待复检(已知 30~40 分钟轮询延迟)
AI DevSpace OCR 读到命令回显 is_finish 待复检 待复检

5. 参数对照表

参数 取值 后果 建议
PBKDF2 迭代 cc 1003 / 1 macOS 用 1 → 0/168 成功 按平台试两个候选,靠填充校验自动判定
expires 字段 不传 / -1 / 未来秒 -1 会被静默丢弃 仅 exp>0 且未过期 时传
domain 前导点 .huaweicloud.cn vs huaweicloud.com 丢失子域覆盖 原样保留导出值
导出范围 单站 48 条 / 全域 163 条 单站缺 auth 域 → 判闸 FAIL 按判定依赖的域导出,不按"当前站点"
判定观测 脚本日志 / 页面 / 明细 日志单独使用 → 假成功 三跳齐备才结算
external_check 延迟 30~40 分钟 立刻重跑会重复动作 只等,不改脚本

6. 边界条件

  1. 票据有效期 ≠ cookie 到期时间。会话 cookie 的 expires=0 只说明"浏览器关掉就清",服务端 session 是否还认要由页面/接口回答,不能从 SQLite 推断。
  2. 两个 Chrome profile 共用系统钥匙串密钥,所以本地解密可行;换机器、换用户、换钥匙串条目后不成立,不要把这个方案当通用凭据同步手段。
  3. 只读备份优先:用 sqlite3 .backup 拿一致性快照再读,别在运行中的库上直接查,-wal 未落盘时会读到不完整状态。
  4. 注入是覆盖语义:同名同域同 path 会被覆盖。恢复登录时这是想要的;做对照实验时要先记基线。
  5. SUCCESS 之外还要看分母:INJECT_OK 48 缺了「应该有多少条」这个分母,所以看不出少了整个 auth 域。
  6. 外部轮询型判定(external_check)天然滞后,动作已完成而状态仍 todo 是正常态,不构成立即重试的理由。

7. 排查清单(下次遇到"登录失效/0 分"照这个走)

  • [ ] 先把结论降级为「未确认」,不要直接写「票据过期」「方法无效」。
  • [ ] 分别列出 CdiskC_{disk}、CliveC_{live},确认关键票据在运行时里,而不是只在文件里。
  • [ ] 核对域覆盖:把导出文件的 domain 去重打印,和判定依赖的域做差集。
  • [ ] 解密类操作先看加密前缀(v10/v20),再按平台试参数;全量失败优先怀疑参数,不是数据。
  • [ ] session cookie 注入不传 expires;注入后用 context.cookies() 反查条数。
  • [ ] 页面判闸用「昵称 + 积分数字 + 无登录墙」三条件,别用"有没有密码框"这种单条判据。
  • [ ] 动作完成后查服务端 is_finish / today_status,最后查积分明细;三者不同步时以明细为准。
  • [ ] 状态滞后但服务端已 done 时只等;服务端长期 todo 且历史明细从无该条目时,才补正确入口动作。
  • [ ] 任何"重跑"前,先到列表页确认写入是否已落地,避免重复灌水。

8. 收尾

这次从「今天 0 分、票据过期」到 LOGIN_GATE OK,中间没有重新登录、没有扫码、没有改账号,只是把已经存在但没被放进正确位置的票据补齐,并且不再相信脚本自己打印的 SUCCESS。

对自动化系统来说,最贵的不是失败,是把失败归因错之后的反复重做。日志只能证明代码走到哪一行;要证明事情办成了,永远需要一个独立于写入路径的观测点,以及一个愿意等状态同步的窗口。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。