返回博客

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 触发时你的选择器早就命中了。

坑:内容平台的 DOM 不是鉴权状态的信源

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 是服务端颁发的,匿名用户不会有

不同平台的 session cookie 名称各异,需要手动确认——登录前后在 DevTools → Application → Cookies 做一次 diff,只有登录后才出现的那个 cookie 才是真实的 session 标记。不要凭名字猜,idtstc 之类的通用名称大概率是匿名指纹 cookie。

实测:如何找到目标站点的 session cookie

  1. 打开 DevTools → Application → Cookies,截图记录登录前的 cookie 列表
  2. 手动扫码完成登录
  3. 再次打开 Cookies,与截图对比
  4. 只在登录后才出现的 cookie 就是 auth cookie——优先选名称语义明确的(sessionauth_tokenSUB 等),避开 tcidbuild 之类的匿名指纹

常见平台参考(仅供校对,务必以 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。