Playwright 登录态突然失效:storageState 过期与作用域排查
登录态复用是 Playwright 里最容易「一次配好、半年不管」的东西,也因此最容易在某个早上突然全线飘红:几十个用例齐刷刷停在登录页,而你上周什么都没改。这类故障的排查顺序其实很固定,因为 storageState 这个文件的结构、它被谁写、被谁读,仓库文档里都写得很死。下面按现象往下走。
现象长什么样
典型有三种样子,值得先分清:
- 所有 project、所有用例一起失败,停在登录页或跳回登录页;
- 只有某一个 project(比如 firefox)失败,chromium 是好的;
- 本地跑没问题,CI 上失败;或者反过来,命令行跑没问题,UI 模式里失败。
第一种偏向 token 过期,第二种偏向作用域/浏览器绑定,第三种偏向状态文件根本没被重新生成。
第一步:把状态文件当成数据结构读,而不是黑盒
docs/src/api/class-browsercontext.md 里 BrowserContext.storageState 的返回值签名写得很清楚,它是一个对象:cookies 是数组,每一项有 name、value、domain、path、expires、httpOnly、secure、sameSite;origins 也是数组,每一项有 origin 和 localStorage(里面又是 name / value 对)。文档对 expires 的说明是「Unix time in seconds」。
这几行签名直接给出了两个可执行的判定动作。
动作一:看 cookies 里的 expires。 打开你的状态文件(按 docs/src/auth.md 的建议,它通常在 playwright/.auth/ 目录下),找到承载会话的那条 cookie,把 expires 这个秒级时间戳换算成本地时间。如果它已经落在过去,那就是过期,不用再往下查了。
动作二:看 origins 里的 origin 值。 如果你的登录态是放在 localStorage 里的,它是按 origin 归档的。把这里的 origin 和你测试实际访问的地址(配置里的 baseURL)比一比,端口、协议、子域任何一处不同都是不同的 origin。
这里有一处值得单独指出来,因为它是两段签名放在一起才看得出来的:cookies 那一层有 expires 字段,origins[].localStorage 那一层只有 name 和 value,没有任何表示有效期的字段。也就是说,如果你的应用把 JWT 放在 localStorage 里,状态文件本身不携带这个 token 什么时候失效的信息,Playwright 也就没有依据替你判断该不该重新登录——文件永远「看起来是新的」,只有服务端会拒绝你。这类项目必须自己定策略,不能指望框架发现过期。
动作三:在运行中打印 cookie。 class-browsercontext.md 里 BrowserContext.cookies 接受一个可选的 urls 参数,文档写明「如果不指定 URL 就返回全部 cookie,指定了就只返回影响这些 URL 的 cookie」。这一条正好可以用来验证作用域——传入你实际访问的那个 URL,如果返回里没有会话 cookie,那就是 domain/path 没匹配上,而不是 cookie 不存在。
第二步:三类原因分别怎么处置
token 过期
docs/src/auth.md 在讲共享账号那一节的末尾直接写了一句:状态过期时你需要删掉存下来的状态;如果不需要在多次运行之间保留状态,就把浏览器状态写到 TestProject.outputDir 下面,那个目录在每次运行前会被自动清理。docs/src/test-api/class-testproject.md 对 outputDir 的描述与之对应——「This directory is cleaned at the start」,并且每个用例会在其中拿到独立子目录,保证并行时不冲突。
所以这里其实是一个二选一的设计决定,而不是 bug:想跨轮次复用,就要自己承担清理责任;不想管,就把它写进 outputDir,用「每次重新登录」换掉「过期判断」。
还有一个高频场景:UI 模式。auth.md 写明 UI 模式默认不会运行 setup project,文档给出的做法是先在过滤器里启用 setup project,点 auth.setup.ts 旁边的三角按钮手动跑一次,再把 setup project 从过滤器里关掉。如果你只在 UI 模式里遇到未登录,先查这一条。命令行侧对应的坑是 --no-deps:docs/src/test-projects-js.md 写明这个选项会忽略所有 dependency 与 teardown,只跑你直接选中的 project——带上它,setup 就不会跑,用的还是磁盘上那份旧文件。
cookie 的 domain / path 不匹配
docs/src/api/params.md 里 storageState 作为输入选项时的字段说明比返回值更严格,逐字写着:domain 与 path 是必填的(“Domain and path are required”),并且「要让 cookie 对所有子域也生效,给 domain 加一个前导点,像这样:.example.com」。而 docs/src/api/class-browsercontext.md 里 BrowserContext.addCookies 的参数说明是另一种写法——url 或者 domain + path 二选一必须给其一。
这意味着两件事:手工拼装或者用脚本改写状态文件时,漏掉 path 是无效的写法;测试环境从 example.com 换到 app.example.com 这种子域迁移,旧状态文件里不带前导点的 domain 不会自动跟过去。
作用域问题还有一类更隐蔽的:存储介质没被抓进来。auth.md 说明复用的状态覆盖 cookies、local storage、IndexedDB 和基于 WebAuthn 的 passkey,但 IndexedDB 需要显式开启——class-browsercontext.md 里 BrowserContext.storageState 的 indexedDB 选项标注 since: v1.51,说明写明「设为 true 可把 IndexedDB 纳入状态快照,如果你的应用用 IndexedDB 存认证 token(比如 Firebase Authentication),请启用它」。同理,虚拟 WebAuthn 凭据对应的 credentials 选项标注 since: v1.61。这两个选项不开,抓下来的文件里就是没有那部分数据,表现和「登录态失效」一模一样。
而 session storage 是明确的例外:auth.md 写明 Playwright 不提供持久化 session storage 的 API,只给了一段用 page.evaluate 取出、再用 context.addInitScript 灌回去的代码片段。如果你的应用把会话放在 session storage,那它从一开始就不在 storageState 的覆盖范围内。
多个 project 共用一份 state
auth.md 推荐的基础做法是:一个 setup project,各测试 project 通过 dependencies 依赖它,并在 use 里指向同一个 storageState 文件。但同一节的「When not to use」里给了两条边界,第二条是你的认证是浏览器相关的(“Your authentication is browser-specific”)。chromium / firefox / webkit 共用一份文件,正好踩在这条上。
另一条边界是「你的测试会修改服务端状态」。对应的处置在「一个 worker 一个账号」那一节:用 worker 级 fixture 覆盖 storageState,用 TestInfo.parallelIndex 区分 worker,把文件写到 path.resolve(test.info().project.outputDir, '.auth/${id}.json')。docs/src/test-api/class-testinfo.md 对 parallelIndex 的说明是「worker 在 0 到 workers - 1 之间的序号,同时运行的 worker 保证 parallelIndex 不同;worker 因失败重启后,新进程的 parallelIndex 不变」。注意后半句:重启不换号,所以复用逻辑要能容忍同一个号被再次拿到——auth.md 的示例里正是先 fs.existsSync(fileName) 判断、存在就直接复用。
多角色的情况,auth.md 的处置是在 setup 里认证多次写多个文件,然后在测试文件或测试组里用 test.use({ storageState: ... }) 指定,而不是在配置里设。这一句是有针对性的:写在 config 的 use 里就是全局的,多角色场景下必然互相覆盖。
顺带一个容易漏的联动:docs/src/test-api/class-testoptions.md 在 reuseContext 的限制里写明,只有少数几个 context 选项允许在连续用例之间不同,「在 test.use 里改动其它任何选项,例如 locale 或 storageState,都会静默强制新建 context」。如果你既开了 reuseContext 又按角色切 storageState,行为上不会报错,但那份复用不成立。要注意的是这个选项在文档里被标成 Experimental,同时挂了 discouraged 标记,文档自述它是为驱动组件测试的 story gallery 而设、端到端测试建议不要开——所以真遇到登录态问题时,它更应该被当成需要排除的变量,而不是要保住的优化。
第三步:改完之后怎么验证
auth.md 的 setup 示例里给的两种收尾动作就是验证手段,别删掉它们:一是 await page.waitForURL('https://github.com/'),注释写明「有时登录流程会在若干次重定向的过程中才把 cookie 设上,等到最终 URL 才能确保 cookie 真的写好了」;二是 await expect(page.getByRole('button', { name: 'View profile and more' })).toBeVisible(),等页面进入所有 cookie 都已就绪的状态。上面这些是仓库示例里针对 GitHub 登录页的具体值,换成你自己站点的选择器即可。
再补两个对照动作:
- 用 project dependencies(而不是
globalSetup)做认证时,test-projects-js.md写明 reporter 会显示 setup 用例,trace viewer 会记录 setup 的 trace,可以直接翻 DOM snapshot 看登录那一步到底停在哪。docs/src/test-global-setup-teardown-js.md的对比表里,globalSetup这一列在「HTML report visibility」与「Trace recording」两行都是不支持。想查登录问题,这一点是实打实的差别。 - 做一个「本来就该未登录」的对照文件:
auth.md给了test.use({ storageState: { cookies: [], origins: [] } })这种重置写法。如果这个文件的断言正常、而登录用例不正常,说明链路没坏,问题就在状态数据本身。
Windows 侧提两点。auth.md 为创建 playwright/.auth 目录分别给了 bash、batch(md playwright\.auth)和 PowerShell(New-Item -ItemType Directory -Force -Path playwright\.auth)三种写法,路径分隔符不同,照抄对应的那一份就行。更需要注意的是相对路径解析:params.md 对 storageState 的 path 选项写明「如果是相对路径,则相对当前工作目录解析」。CI 里的工作目录和你本地未必一致,这是「本地好、CI 挂」的常见来源。.NET 侧 auth.md 的 C# 示例注释也点了同一件事——测试在 <TestProject>\bin\Debug\netX.0\ 下执行,所以示例里用的是 ../../../playwright/.auth/state.json 这种相对路径回到项目根。
什么情况说明不是这个原因
以下几种情况,继续在 storageState 上折腾是白费力气:
- 会话存在 session storage 里。仓库明说没有持久化 API,那就不属于状态文件失效,得走
addInitScript那条路。 setupproject 自己就失败了。test-projects-js.md写明依赖失败时,依赖它的 project 根本不会运行。这时你看到的是「没跑」,不是「跑了但未登录」,reporter 里能分清。- 只有个别用例失败,其它用例登录态正常。共用一份状态文件的场景下,过期与作用域问题是全局性的;个别失败更可能是这条用例自己改了服务端状态——这正是
auth.md把「测试会修改服务端状态」列为共享账号「不适用」条件的原因。 - 换个浏览器就好。那是浏览器相关的认证,对应
auth.md明确列出的第二条不适用条件,处置方向是按 project 拆状态文件,而不是延长有效期。 - 状态文件里的 cookie 时间戳还在未来、
origin也对得上,但仍然未登录。这时问题多半在服务端会话已被使主动失效,客户端数据是否新鲜已经说明不了什么,storageState 这一层查不出来。
最后一句提醒:状态文件里装的是能冒充你或你的测试账号的 cookie 和请求头,auth.md 用 danger 提示框写明「强烈不建议把它们提交到私有或公开仓库」,并建议把 playwright/.auth 加进 .gitignore。排查过程中把状态文件贴进 issue、发到群里,是比登录态失效严重得多的事故。
安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。
本文依据 github.com/microsoft/playwright 仓库于 2026-08-18 的公开内容整理,
事实来自仓库内的文档与源码。我们没有对文中涉及的功能做过实测,
因此不涉及运行速度、稳定性与实际表现的任何描述。
该项目迭代频繁,文中涉及的 API 签名、配置项与默认值随版本变动,请以仓库最新内容为准。
本文不涉及浏览器版本矩阵与版本清单,相关信息请以官方发布说明为准。