Playwright 登录检测假阳性:DOM 选择器骗了你,session cookie 才是真相
给某内容平台写 Playwright 自动登录脚本,逻辑看起来无懈可击:导航到首页,等到头像、用户链接、.user-info 之类的选择器出现,就认为登录成功,保存 storageState.json。脚本跑起来,3 秒内报「✅ 登录成功」,喜滋滋往下走——然后所有需要鉴权的 API 请求一律返回 401。
现象
// ❌ 错误写法 — 用 DOM 检测登录状态
await page.goto('https://example-content-site.com/explore')
const successSelectors = [
'a[href*="/user/profile/"]',
'.user-info',
'.avatar img',
]
while (Date.now() - start < timeout) {
for (const sel of successSelectors) {
const el = await page.$(sel)
if (el && await el.isVisible()) {
await ctx.storageState({ path: STATE_FILE })
return { loggedIn: true } // 🎉 ...假成功
}
}
await page.waitForTimeout(2000)
}检查保存下来的 storageState.json:
cookies: ['a1', 'abRequestId', 'webBuild', 'webId', 'xsecappid', 'acw_tc', 'unread', 'sec_poison_id']
没有 web_session。用户根本没有扫二维码,脚本却已经报成功了。
根因
现代内容/社交平台对匿名访客也会渲染大量「用户形态」元素——推荐创作者卡片、热门用户头像、侧边栏「登录/我的」入口——这些都是为了丰富探索体验和 SEO 而做的服务端渲染。页面 HTML 从第一个字节起就带着这些内容,DOMContentLoaded 触发时你的选择器早就命中了。
a[href*="/user/profile/"] 命中的可能是推荐创作者列表中的别人。.avatar img 命中的可能是热门帖子作者的头像。.user-info 命中的可能是「点击登录」弹窗的占位区。这些元素匿名用户都看得到,完全无法区分是否真正登录。
正解
轮询后端颁发的 session cookie,而不是 DOM。Session cookie 只有在完整的认证握手(扫码 → 服务端验证 → Set-Cookie)之后才会出现,匿名浏览器无法伪造它。
// ✅ 正确写法 — 轮询 session cookie
const start = Date.now()
while (Date.now() - start < timeoutMs) {
const cookies = await ctx.cookies('https://example-content-site.com')
const session = cookies.find(c =>
c.name === 'web_session' && c.value && c.value.length > 0
)
if (session) {
await ctx.storageState({ path: STATE_FILE })
return { loggedIn: true }
}
await page.waitForTimeout(2000)
}
throw new Error('login_timeout')browserContext.cookies(url) 实时读取浏览器 cookie jar,传入 URL 可以把匹配范围限定在目标站点,避免撞上其他域名的同名 cookie。
不同平台的 session cookie 名称各异,需要手动确认——登录前后在 DevTools → Application → Cookies 做一次 diff,只有登录后才出现的那个 cookie 才是真实的 session 标记。不要凭名字猜,id、ts、tc 之类的通用名称大概率是匿名指纹 cookie。
实测:如何找到目标站点的 session cookie
- 打开 DevTools → Application → Cookies,截图记录登录前的 cookie 列表
- 手动扫码完成登录
- 再次打开 Cookies,与截图对比
- 只在登录后才出现的 cookie 就是 auth cookie——优先选名称语义明确的(
session、auth_token、SUB等),避开tc、id、build之类的匿名指纹
常见平台参考(仅供校对,务必以 diff 结果为准):
| 平台 | Session cookie | 匿名指纹(不要用) |
|------|--------------|-----------------|
| 小红书 | web_session | a1, webId, xsecappid |
| 抖音 | sessionid / sessionid_ss | ttwid, s_v_web_id |
| Twitter/X | auth_token | guest_id, gt |
| LinkedIn | li_at | bcookie, lidc |
| Instagram | sessionid | ig_did, csrftoken |
验证双保险
登录后跑一个正/负用例验证,避免自欺欺人:
// 负用例——空 cookie 的请求应该 401
const ctx2 = await browser.newContext()
const r1 = await ctx2.request.get('https://api.example-content-site.com/v1/user/me')
console.log('匿名:', r1.status()) // 预期 401 / 403
// 正用例——带 storageState 的请求应该 200
const ctx3 = await browser.newContext({ storageState: STATE_FILE })
const r2 = await ctx3.request.get('https://api.example-content-site.com/v1/user/me')
console.log('已登录:', r2.status()) // 预期 200如果负用例也返回 200,说明你选的接口是公开的,换一个需要鉴权的端点(/me、/notifications 等)。
一句话外卖
DOM 元素谁都能看到,session cookie 只有服务端认证后才颁发——检测登录状态永远轮询 cookie,不要信 DOM。