浏览器 Agent 登录态的持久化与隔离
区分浏览器会话、托管配置与 Playwright 存储快照的持久化边界,并要求恢复身份后验证实际登录状态。通过单写入者策略、身份归属和访问目标限制,降低并发覆盖、跨客户串号及凭据泄露风险。
AI 深度解读
让 AI 浏览器记住登录,并不等于让它可靠地使用账号。Cookie 还在,服务端登录可能已经失效;两个任务共用一份浏览器档案,也可能在结束时互相覆盖。更麻烦的是,任务能正常打开网站,却用错了客户的账号。
这篇教程用 Scrapeless Browser 和 Playwright 拆解这些问题。它的核心主张是:先明确状态能保存多久、属于谁、由谁更新,再谈复用登录。对正在做浏览器 Agent、自动化采集或账号后台操作的团队,这套边界比「登录一次后保存 Cookie」更有参考价值。

保存了登录状态,为什么还是不能直接开工
作者先区分了三种容易混用的状态机制。
活动浏览器会话保留当前实例,包括打开的标签页、内存里的页面状态和正在进行的操作流程。浏览器结束,这个运行环境也就结束。
Scrapeless 的托管档案(Profile)保存受支持的浏览器数据,供后续会话加载。它需要显式控制是否保存修改,不能把「加载档案」和「更新档案」当成同一件事。
Playwright 的存储快照(storage snapshot)则把受支持的存储序列化。常见内容有 Cookie 和 localStorage,通过 indexedDB: true 可以包含 IndexedDB。它不是完整浏览器备份,不能恢复所有标签页、内存对象或扩展。sessionStorage 还需要单独处理,普通认证快照流程不会自动解决所有应用的登录机制。
选择哪一种,取决于工作流要延续什么:接着操作当前页面、下次重新打开账号,还是把状态搬到另一个受控环境。
但三种机制都不能保证登录仍然有效。服务端可以撤销会话,应用也可能依赖快照之外的数据。恢复状态之后,必须让目标应用重新确认当前账号。
账号归属要由业务系统决定,不能让模型挑档案
作者建议在分配浏览器之前,先通过业务系统的账号记录确定档案。记录至少关联租户、批准访问的源(origin)、账号标识、Profile 标识和当前访问状态。
两个客户即使访问同一个网站,也不能共享错误的账号状态。Worker 应接收系统分配的 Profile,不能从页面内容或模型生成的动作里随意选一个。
初始化档案时,作者要求使用获准的账号登录,完成 MFA,核对身份,再通过支持的生命周期关闭会话。Scrapeless 使用 profilePersist: true 保存修改,档案数据在会话关闭时保存;后续读取任务可以设置 profilePersist: false。
这里有个很容易踩的坑:profilePersist: false 只控制是否回写浏览器档案。浏览器照样可以点击删除按钮、提交表单、修改服务端数据。允许执行哪些动作,要另外约束。
示例依赖 Node.js,安装命令为 npm init -y 和 npm install playwright-core@1.64.1 @scrapeless-ai/sdk@1.12.1。环境变量包括 SCRAPELESS_API_KEY、SCRAPELESS_PROFILE_ID、ACCOUNT_URL、EXPECTED_ACCOUNT_ID,可选 ACCOUNT_SELECTOR;保存为 authenticated-state.mjs,运行 node authenticated-state.mjs。自有且获准使用的 CDP 浏览器也可通过 CDP_WS_ENDPOINT 接入。
远程会话示例配置了 sessionName: 'authorized-account-reader'、sessionTTL: 180、proxyCountry: 'US'、profileId 和 profilePersist: false,随后通过 chromium.connectOverCDP(endpoint, { timeout: 30000 }) 连接,取默认 context 新建页面。
下面这段解决的是「页面能打开,但账号可能不对」的问题,检查发生在采集和账号操作之前:
const response = await page.goto(target.href, {
waitUntil: 'domcontentloaded', timeout: 30000,
});
if (!response || !response.ok()) throw new Error('Account page did not load');
if (new URL(page.url()).origin !== target.origin) {
throw new Error('Unexpected authentication destination');
}
const identity = page.locator(
process.env.ACCOUNT_SELECTOR ?? '[data-testid="account-id"]'
);
await identity.waitFor({ state: 'visible', timeout: 10000 });
if ((await identity.innerText()).trim() !== accountId) {
throw new Error('Active account does not match the assigned account');
}
要看的是最后的精确账号匹配,默认选择器只是自有应用的示例契约。「欢迎回来」或 URL 没变都不足以确认身份。最终 origin 检查也只是事后发现跳转;要阻止未批准的请求,需要浏览器或网络层控制。
多任务复用时,保存、代理和导出要分开管
据作者引用的 Scrapeless Profiles 文档,开启持久化后,最后关闭的会话提供最终保存的数据。两个任务分别修改同一份档案,就会争夺最后一次写入。
作者给出的流程是:刷新期间停止正常分配,必要时等待读取任务结束,获取档案锁,执行登录或续期,关闭写入会话,再启动新的读取会话验证,最后释放锁、恢复分配。这把锁由应用负责,示例没有提供服务商托管的分布式锁。
读取任务也可能修改同一个服务端资源。有些网站在新登录后还会使旧会话失效,所以「不回写档案」不能代替账号并发测试。
代理是另一条边界。Profile 保存浏览器数据,代理决定网络出口。出口地区变化可能改变内容或触发额外验证,但稳定 IP 无法修复过期登录。浏览器跑在 Scrapeless 时使用其路由配置;自行运行浏览器或使用文档支持的自定义路由时,再评估 standalone proxies。Sticky residential 分配有有效期,static ISP 是需要更长期网络分配时的另一种选择,费用也应分开评估。
导出快照同样应有明确用途。示例默认关闭导出,只有 EXPORT_STATE=true 才执行:
if (process.env.EXPORT_STATE === 'true') {
const directory = path.resolve('.auth');
await mkdir(directory, { recursive: true, mode: 0o700 });
const state = await context.storageState({ indexedDB: true });
const file = await open(path.join(directory, 'account.json'), 'wx', 0o600);
try { await file.writeFile(JSON.stringify(state)); }
finally { await file.close(); }
}
要看 wx:文件已存在就拒绝覆盖。目录权限是 0o700,文件权限是 0o600,但这些依赖对应的 Unix 权限模型,不能替代加密、主机访问控制和保留期限;.auth/ 还应加入仓库忽略规则。
快照可传给 browser.newContext({ storageState: ... }) 创建新 context,并不会合并进现有的服务商 context,运行环境是否支持也要确认。原示例最后用 finally 中的 await browser.close() 关闭会话。
我的判断
这篇最有价值的地方,是把账号身份、状态保存、网络出口和动作权限拆开了。对多租户 Agent,首先要保证「正在使用谁的账号」,然后才是减少重复登录。
但它不是一份已经完成云端验证的交付方案。作者明确说,账号检查和状态导出只在本地测试应用上跑过;Scrapeless 档案持久化、真实账号登录和代理出口,都需要读者配置服务后验证。原文也没有与其他方案比较成功率、耗时或成本,不能据此认定托管 Profile 优于 Playwright 快照。
我的取舍是:跨浏览器会话反复复用账号时,可以评估托管 Profile;只需在受控环境间迁移受支持存储时,可以先评估快照。已有适用的官方 API 时,也应比较直接调用 API 的成本,原文没有提供这方面的对照数据。
还有一个必须补进业务流程的点:认证失败应有明确结果,区分身份元素缺失、账号不符、需要 MFA 和应用拒绝授权。不能让模型临时换个 Profile 继续试。撤销账号时,也要停止新任务、处理活动任务、使导出状态失效,并使用目标网站支持的会话撤销能力。删掉本地文件或 Profile,不保证服务端已经退出登录。
能拿来做什么
- 给 Node 浏览器 Worker 增加统一的身份校验入口,把期望账号和允许访问的 origin 作为任务参数,在采集或操作之前完成匹配。
- 在自有前端的账号页面提供稳定身份标识,例如示例中的
data-testid="account-id",让自动化能够核对具体账号。 - 为共享 Profile 的登录续期增加应用级锁,关闭写入会话后,用新读取会话验证保存结果,再恢复任务分配。
- 把认证失败做成结构化任务结果,并按迁移需要决定是否导出快照;需要导出时,同步落实忽略规则、受控存储和失效处理。