cookie 判据会撒谎:CDP 登录态保活的五个实测坑

36 阅读5分钟

一台常驻的 Chrome 跑着十几个平台的内容发布:每个平台一段原子脚本,通过 CDP 注入标题和正文,再点发布。发布链路本身是稳的,不稳的是登录态——Chrome 因为内存水位或者发版重启一次,会话 cookie 没落盘,第二天所有平台一起失效。

第一版修法很朴素:把 cookie 全量导出成 JSON,浏览器起来再全量灌回去。它能跑,但有三个静默失败,日志里全写着"成功"。

坑一:cookie 在,不等于已登录

最早判据是"存在某几个关键 cookie 名就是已登录"。实测这个判据会撒谎。有的站点退出登录后不删本地 cookie,只把服务端会话作废,本地看着齐全、请求全 401;有的站点把凭据放在 localStorage,只搬 cookie 等于没搬;反过来,统计类 cookie 掉几条又完全不影响登录。

现在的判据分三层:cookie 只用来粗筛"肯定没登录过";真判据是打开目标后台页,看落地 URL 和页面关键词;能问平台接口的就问接口,看到底是 401 还是真数据。三层都过才敢说已登录。

坑二:浏览器级端点没有 Network.setCookie

CDP 有两类 WebSocket 端点:page 级和 browser 级。要给整个浏览器灌 cookie,只能用 browser 级。但 browser 端点对 Network 域的支持不完整,实测 setCookie 和 deleteCookies 都直接回 -32601,"wasn't found"。

能用的只有 Storage.setCookies 写、Storage.getCookies 读。这类错误如果被 try/except 顺手吞掉,表现就是"回灌完成、条数 0、账号仍掉线",排查时容易一路怀疑 cookie 本身。

同一段代码里还有一个更隐蔽的:Storage.getCookies 不要传 browserContextId 为 null。同一台浏览器实测,省略这个参数返回 750 条 cookie,显式传空值返回 0 条。它的语义是"给我这个上下文里的",空值并不等于默认上下文。

坑三:去重键只用 name 会漏灌

备份文件存一个 cookie 数组,回灌前要和当前 cookie 比对。第一版拿 cookie 名当键,结果开源中国那 9 条 cookie 里 BEC 同时存在于 4 个不同 domain 和 path 下,按 name 去重只剩一条,另外三条永远补不上,登录态时好时坏。

正确的键是三元组 (name, domain, path)。cookie 的身份本来就由这三者共同定义,name 只是其中一维。

备份合并,回灌只补缺

两边策略是反的,因为风险不同。

备份用合并,当前值优先、旧值补进来。Chrome 刚重启时 cookie 会少一截,直接覆盖会把一份完好的快照冲成残废版本,下次回灌就没东西可用。

回灌只补"当前根本没有"的。同名同域已经存在的一律不动——服务端可能刚轮换过新值,用备份里的旧值盖上去,等于亲手把好端端的登录态打掉。这条是"回灌之后反而掉线"教出来的。写回去只发这一批待补项,一次 Storage.setCookies 批量提交,单条失败才退化成逐条重试。

落盘也做了两件小事:临时文件写完再原子替换,避免进程被杀时留下半个 JSON;权限 0600,这是凭据不是配置。

登录态是数据,不是进程

一开始按"进程活着就等于登录着"设计,结果每次重启都归零。改成落盘之后还有个副产品:浏览器档案里真正的身份部分很小,Cookies、LocalStorage、IndexedDB、Preferences 合起来约 0.4MB,另外九十多 MB 全是网页缓存。缓存可以在低峰期清掉,身份不动,回来自动重新加载,不用重新扫码。

保险箱后来也升到第二版:不只存 cookie,还存 localStorage 和 sessionStorage。因为只搬 cookie 对一部分平台后台无效,凭据在前端存储里,cookie 齐了页面照样把你踢回登录页。

多账号共用一台机器,隔离要落在目录上

同一台机器跑多个账号时,最危险的不是"cookie 串了",而是"看起来是同一个 profile"。做法是每个账号一个独立的 user-data-dir,平台自营账号单独一个目录,登录态按目录存、按目录回灌,脚本连 CDP 时只认从该目录启动出来的那个端口。再加一道绑定校验:账号与 cookie 指纹不符就拒绝执行,宁可不发也不串号——串号发到别人账号上,代价远大于一次失败。

坑四:调度器里的 Python 不是你以为的那个

脚本挂在 cron 上跑。环境里默认的 python3 版本低,也没装 websocket-client。现象是手动跑一切正常,定时任务连 CDP 都连不上,日志只有一行 ImportError。做法是顶部自检,缺依赖就 execv 到带依赖的解释器,同一个进程原地换壳,不改调用方,顺手把 PATH 写死。

验收:把"已登录"做成可读的断言

现在每 15 分钟跑一次 sync:已登录的平台存快照;有快照但掉线的补齐缺失 cookie,补完再判一次,还不登录就进问题列表上报。巡检输出是一张表,一行一个平台,带条数和备份时间。黄色是有备份但当前没登录,扫码就能救回来;红色是从来没登录过,得从零绑号。这两档责任人完全不同,混在一起报,值班的人不知道该找谁。

三条可迁移的经验

这类保活代码最容易写成"看着成功"的样子:条数写了、日志绿了、实际一条 cookie 都没进去。经验只有三条。

判据要分层,cookie 只能当粗筛,落地页和平台接口才是真判据。

身份用复合键,三元组少一维就会漏。

恢复永远是补缺而不是覆盖。覆盖是把不确定当确定,代价往往要过一天才显现。