Playwright 持久化会话实战:让自动化脚本稳定保持登录态
背景:自动化的痛点不是选择器,是登录态
做过 Web 自动化的同学大概都有同感:真正难写的不是元素定位,而是让脚本稳定地保持登录状态。
最常见的写法是这样的:
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto(LOGIN_URL);
await page.fill('#username', USER);
await page.fill('#password', PASS);
await page.click('#submit');
这段代码每次运行都是一个全新的、干净的浏览器上下文。带来的问题很直接:
- 每次都要重新走一遍登录流程,慢且脆弱;
- 频繁登录会触发风控——短信验证码、滑块验证、甚至临时限制账号;
- 登录后拿到的 Cookie 一关浏览器就没了,下次还得再来一遍。
本文记录一套经过实践验证的方案:用持久化用户目录 + storageState 双保险,把登录态变成可复用的资产。
一、核心思路
把登录态拆成两层来看:
| 层次 | 内容 | 载体 |
|---|---|---|
| 浏览器层 | Cookie、localStorage、IndexedDB、设备指纹、信任标记 | user-data-dir 目录 |
| 应用层 | 序列化后的 Cookie + localStorage 快照 | storageState JSON |
两层各有用处:前者用于同机复用,后者用于跨机迁移。
二、方案一:持久化用户目录
Playwright 提供了 launchPersistentContext,指定一个目录,浏览器就会把所有状态写进去:
const ctx = await chromium.launchPersistentContext('./profile', {
headless: false,
viewport: { width: 1440, height: 900 },
locale: 'zh-CN',
timezoneId: 'Asia/Shanghai',
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...',
args: ['--disable-blink-features=AutomationControlled'],
});
关键在于:这个目录不要删、不要换路径。
平台的设备信任标记(通常是某些长效 Cookie)就存在这里。以我实测的站点为例,一个持久化 Cookie 的有效期直接给到了一年多,只要目录还在,这段时间内基本不会再要求二次验证。
三、方案二:storageState 导出与回灌
持久化目录的问题是不便迁移——换台机器、换个容器就没了。这时用 storageState:
// 导出:把当前会话快照存下来
await ctx.storageState({ path: './session/state.json' });
// 回灌:启动后把 Cookie 注回去
const st = JSON.parse(fs.readFileSync('./session/state.json', 'utf8'));
if (st.cookies?.length) await ctx.addCookies(st.cookies);
有一个很容易被忽略的细节:
storageState会连 Session 级 Cookie(expires = -1)一起导出,但浏览器正常关闭时这些 Cookie 是会丢的。
所以正确姿势是两个方案叠加:
- 用持久化目录保住设备指纹和长效 Cookie;
- 每次启动时再把
state.json里的 Session Cookie 回灌一次。
实测下来,光靠 state.json 在一个全新的临时目录里也能恢复登录态——这对容器化部署很关键。
四、指纹一致性:比 Cookie 更容易翻车的地方
这一条是踩坑最多的地方。风控不只看 Cookie,还看你的浏览器"长得像不像原来那个"。
必须保持一致的参数:
- User-Agent:不要用 Playwright 默认 UA,直接写死一个真实 Chrome 的 UA;
- viewport:每次固定,别这次 1280×720 下次 1920×1080;
- locale / timezone:和账号常用地区一致;
- 启动参数:固定下来,别随机增删。
把这些集中到一个配置对象里,避免散落在代码各处:
const CFG = {
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36',
viewport: { width: 1440, height: 900 },
locale: 'zh-CN',
timezoneId: 'Asia/Shanghai',
};
还有一个容易被忽视的点:出口 IP 尽量稳定。频繁切换代理节点,比 Cookie 失效更容易触发风控。
五、抹掉自动化特征
基础的对抗手段,成本很低但有效:
await ctx.addInitScript(() => {
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
});
配合启动参数:
args: [
'--disable-blink-features=AutomationControlled',
'--no-first-run',
'--no-default-browser-check',
]
这几行不保证能绕过所有检测,但至少不会在第一步就被一眼认出。
六、完整的会话管理封装
把上面的经验收拢成一个可复用的模块:
async function open({ headless = false } = {}) {
const ctx = await chromium.launchPersistentContext(CFG.profileDir, {
executablePath: CFG.exe,
headless,
viewport: CFG.viewport,
locale: CFG.locale,
timezoneId: CFG.timezoneId,
userAgent: CFG.userAgent,
args: ['--disable-blink-features=AutomationControlled', '--start-maximized'],
});
// 回灌 session cookie
if (fs.existsSync(CFG.statePath)) {
const st = JSON.parse(fs.readFileSync(CFG.statePath, 'utf8'));
if (st.cookies?.length) await ctx.addCookies(st.cookies);
}
await ctx.addInitScript(() => {
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
});
return ctx;
}
再配一个先判断、后登录的入口逻辑——这是整套方案里最关键的一步:
const ctx = await open();
const page = await visitHome(ctx);
const st = await status(ctx, page);
if (st.loggedIn) {
console.log('会话有效,跳过登录');
} else {
await login(ctx); // 只在失效时才走登录流程
await save(ctx, page); // 登录后立刻固化
}
为什么要"先判断"? 因为反复走登录流程本身就是触发风控的主要原因。会话有效就绝不登录,能省掉绝大多数验证弹窗。
七、踩坑总结
- Session Cookie 会丢:
storageState能导出,但浏览器正常关闭后需要主动回灌。 - 指纹变了等于换设备:UA、viewport、时区任何一个变了,都可能被判定为新设备。
- 别高频重启浏览器:短时间反复启停、反复访问,很容易被临时限制。实测中我曾因高频操作触发过一次临时风控提示,间隔放慢后恢复正常。
- 操作节奏要像人:输入用
type()加延迟,别用fill()瞬间填完;关键操作之间留出间隔。 - 合规使用:这套方案用于自己账号的自动化、测试和运维是合理的;多账号刷量、绕过平台限制属于违规行为,会导致账号被封,不要碰。
结语
持久化会话的核心就一句话:把"登录"当成一次性的、昂贵的操作,把"复用"当成常态。
user-data-dir保住设备和长效凭证;storageState提供可迁移的快照;- 指纹一致性保证平台认得你;
- "先判断后登录"避免无谓的风控触发。
四者配合,脚本的稳定性会有质的提升。
本文为实践记录,代码片段已做脱敏处理。不同平台的 Cookie 策略和风控强度差异较大,具体参数请以实际测试为准。
- 点赞
- 收藏
- 关注作者
评论(0)