ego (lite) — это просто браузер, а ego — ваш личный агент на всех устройствах.
Записаться в лист ожидания
PlaywrightАутентификацияstorageStateАвтоматизация браузераСессии со входом

Аутентификация в Playwright: войдите один раз, переиспользуйте сессию и пройдите аутентификацию заново, когда она истечёт

16 сент. 2026 г.14 минута чтения
Пиксельные персонажи Playwright и ego (lite) рядом с экраном входа на ноутбуке, видна надпись Agent is in control

Когда тесту или скрейперу Playwright нужна аутентификация, обычно нет смысла входить с нуля на каждом запуске. Обычный подход: войти один раз, сохранить cookies и связанное состояние браузера через storageState, затем загружать это состояние в следующих запусках, чтобы переиспользовать ту же сессию. Но наличие сохранённого файла storageState не значит, что вход будет работать бесконечно. Сессия может истечь, быть отозвана сервером или с самого начала загрузиться неправильно.

Если вы автоматизируете собственный аккаунт и уже вошли в браузер, которым пользуетесь каждый день, есть другой вариант: переиспользовать эту существующую сессию браузера напрямую. ego (lite) позволяет ИИ-агентам работать в браузере, где нужное состояние входа уже есть, и при этом держит каждую задачу изолированной в собственном Space. Если задача доходит до кода подтверждения, входа по QR или другого шага, где нужны вы, можно перехватить управление в видимом браузере вместо попытки автоматизировать MFA.

Какой бы подход вы ни выбрали, цель одна: убедиться, что автоматизация действительно имеет действующую аутентифицированную сессию. Запуск, который внезапно возвращает на страницу входа, API, который отвечает 401, или элемент после входа, который так и не появляется, могут выглядеть как три разные проблемы: навигация, права доступа или проблема селектора, хотя на деле все они могут указывать на одну и ту же сломанную сессию. В разделах ниже мы на повторяемой локальной установке покажем, как сохранять, переиспользовать, проверять и обновлять состояние аутентификации в Playwright.

Как выглядит сломанный вход в Playwright

В запуске фикстуры одна отозванная сессия дала все три симптома сразу. После очистки сессии на стороне сервера контекст, переиспользующий сохранённое состояние, был перенаправлен на страницу входа с параметром next, указывающим обратно на дашборд, зонд к аутентифицированному JSON-эндпоинту вернул 401, а ожидание таблицы дашборда сорвалось по таймауту через настроенные 3 секунды.

На публичном логине the-internet тот же класс сбоя это bounce. Открытие /secure без живой сессии приводит на /login с красной вспышкой You must login to view the secure area. Это проверка URL из выноски ниже: браузер на форме входа, а не на странице, которую ждал locator. Этот скриншот и есть bounce. Он не доказывает, что Logout удалил файл storageState. Logout на the-internet завершает сессию текущего окна; файл уже записанный на диск может снова открыть /secure, пока сама cookie не мертва.

the-internet.herokuapp.com/login после запроса к защищённой зоне, красная вспышка You must login to view the secure area
Открытие защищённой страницы без живой сессии отбросило на /login. Красная вспышка это признак. Это отсутствующая или мёртвая сессия, не сломанный locator и не Logout, который удаляет сохранённый файл storageState.

Третий симптом самый дорогой. Таймаут локатора на элементе после входа не говорит, что локатор неверный; он говорит, что страница перед вами не та, которую вы предполагали. В запуске выше таблица так и не отрисовалась, потому что браузер стоял на форме входа. Считать это проблемой селектора значит чинить запрос, который никогда не был сломан.

Четыре корневые причины сбоев аутентификации

Сбои аутентификации собираются в четыре причины, и у каждой свой ремонт. Классификация до патча не даёт превратить исправление в ещё один wait.

1. Состояние так и не загрузилось

Контекст, созданный без storageState, стартует разлогиненным, что бы ни происходило в предыдущем запуске. Тот же итог получается, если сохранить файл не в тот момент, использовать относительный путь, который резолвится в другое место, или выполнить шаг setup в проекте, чьё состояние не передаётся зависимому проекту. Признак: совершенно новый контекст ведёт себя так же, как падающий: оба разлогинены.

2. Cookie есть, но он мёртвый

Сессионный cookie может существовать, иметь верный домен и флаги и всё равно быть отвергнут сервером. У сессионных cookies свой срок жизни, и сервер может отозвать один в любой момент, в том числе при redeploy, смене пароля или idle timeout. Поле expires у cookie это подсказка, а не гарантия: серверная сессия может умереть раньше. Именно этот случай фикстура воспроизводит явным вызовом revoke.

3. Изоляция между контекстами

Cookies и localStorage принадлежат контексту браузера, а не браузеру. Сохранить состояние из одного контекста и ждать, что иначе настроенный контекст им поделится, значит молча проиграть. То же для параллельных workers: каждый worker получает свой контекст, поэтому каждому нужен свой файл состояния или свой шаг входа. Эта изоляция это возможность. Она же причина, почему сессия, которая работает в одном месте, кажется исчезнувшей в другом.

4. Цепочки редиректов SSO

При single sign-on нужное приложение редко является origin, который держит сессию. Вход редиректит на провайдера идентификации на другом домене, провайдер ставит свои cookies, и управление возвращается в приложение с кодом или токеном. Сохранение состояния до завершения этой цепочки фиксирует часть сессии и теряет cookies IdP, а следующий запуск воспроизводит редирект. Надёжный шаблон: дождаться итогового URL приложения и маркера аутентификации перед сохранением, чтобы файл состояния нёс каждый origin, участвовавший в цепочке.

Войдите один раз и сохраните состояние правильно

Поток входа один раз короткий: перейти, аутентифицироваться, подтвердить аутентифицированное состояние, затем записать storageState в файл. Шаг подтверждения отделяет файл состояния, который работает, от того, который иногда не работает.

import { chromium } from "playwright";

const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();

await page.goto("https://app.example.com/login");
await page.fill("input[name=email]", process.env.APP_EMAIL);
await page.fill("input[name=password]", process.env.APP_PASSWORD);
await page.click("button[type=submit]");

// Wait for the authenticated destination, not just for the click to land.
await page.waitForURL("**/dashboard");

// Verify before saving: an authenticated element plus a 200 from the API.
await page.waitForSelector("#revenue-table");
const probe = await context.request.get("https://app.example.com/api/summary");
if (probe.status() !== 200) throw new Error("login did not take");

await context.storageState({ path: "playwright/.auth/user.json" });
await browser.close();

В запуске фикстуры этот шаг занял 119 миллисекунд от открытия страницы входа до записи файла состояния. В файле был один cookie, идентификатор сессии, который выдал сервер, и зонд вернул 200. Такие числа это свойства фикстуры, но форма проверки переносится: проверьте URL назначения, проверьте видимый аутентифицированный элемент, проверьте аутентифицированный эндпоинт и только потом сохраняйте.

Headed-запуск ниже шёл на the-internet.herokuapp.com, а не на дашборд фикстуры. 119 миллисекунд остаются у фикстуры. Скриншот показывает ту же форму сохранения на публичном логине: /secure после входа, видимый Logout и Claude, который проверяет /tmp/pw-auth.json.

Claude Code рядом с headed-окном Chromium на the-internet.herokuapp.com/secure после входа, Logout виден, Claude проверяет /tmp/pw-auth.json
После входа headed Chromium остался на /secure. Claude проверял /tmp/pw-auth.json. Именно этот файл потом повторно использует новый context.

Для тестовых наборов та же идея выражена setup-проектом, который производит файл, и зависимыми проектами, которые его потребляют: это шаблон из официального руководства по аутентификации. Для скриптов и скрейперов последовательность выше и есть весь поток. В любом случае файл является учётными данными: в нём живые сессионные cookies, поэтому относитесь к нему как к паролю и не кладите в систему контроля версий.

Переиспользование сохранённого состояния в тестах и скрейперах

Переиспользование это изменение в одну строку при создании контекста. В скрипте передайте файл в контекст. В наборе тестов Playwright задайте его на проект или на файл теста, чтобы каждый worker стартовал из одного и того же состояния со входом.

// Script: create the context from the saved state.
const context = await browser.newContext({
  storageState: "playwright/.auth/user.json",
});

// Test: use the state for this file (or configure it per project).
test.use({ storageState: "playwright/.auth/user.json" });

В запуске фикстуры новый контекст, созданный из сохранённого файла, достиг защищённого дашборда за 6 миллисекунд без шага входа, и аутентифицированный зонд вернул 200 с ожидаемой нагрузкой. Интересное число не 6 миллисекунд; интересно то, что в потоке не изменилось ничего, кроме источника состояния контекста.

Скриншот повторного использования снят на том же публичном сайте. Новый headed-контекст загрузил /tmp/pw-auth.json, пропустил форму входа и открыл /secure с уже видимым Logout. 6 миллисекунд по-прежнему время фикстуры.

Claude Code создаёт новый контекст Playwright из /tmp/pw-auth.json без заполнения формы входа, рядом с the-internet.herokuapp.com/secure, где Logout по-прежнему виден
Новый headed-контекст загрузил storageState и сразу открыл /secure. Промпт запрещал заполнять форму входа; Logout уже был на странице.

Стоит знать, что файл состояния несёт и чего не несёт. В нём cookies для каждого origin, которого касался контекст, и записи localStorage по origin. В нём нет IndexedDB, session storage и состояния service worker. Приложения, которые хранят токен в IndexedDB, поэтому нуждаются в дополнительном шаге захвата или в повторном входе; ждать, что storageState их покроет, частый источник фантомных сбоев. Сами cookies регулируются доменом, путём и флагами, описанными в руководстве MDN по cookies и справочнике Set-Cookie.

Обнаружение истёкшей или отозванной сессии

Самый дешёвый детектор это аутентифицированный запрос до дорогой работы. У большинства приложений есть небольшой эндпоинт, который отвечает текущим пользователем, счётчиком или объектом конфигурации, и его код статуса это код статуса сессии. Запрос стоит миллисекунды; обнаружить сбой на середине скрейпа стоит весь запуск.

async function sessionIsAlive(context) {
  const probe = await context.request.get(
    "https://app.example.com/api/summary",
    { failOnStatusCode: false },
  );
  return probe.status() === 200;
}

if (!(await sessionIsAlive(context))) {
  await loginAndSaveState(context);
}

Не полагайтесь только на поле expires у cookie. Оно говорит, когда браузер должен перестать отправлять cookie, а это не то же самое, что момент, когда сервер перестаёт его принимать. Отзыв на стороне сервера невидим, пока вы не спросите. Вызов revoke в фикстуре как раз такая ситуация: cookie ещё был на месте и корректно сформирован, а сервер ответил 401.

Автоматическая повторная аутентификация

Автоматическая повторная аутентификация это небольшая машина состояний: обнаружить, войти заново, обновить сохранённое состояние, один раз повторить упавший шаг и проверить. Потолок важен. Рабочий процесс, который молча заново аутентифицируется в цикле, может скрыть неверный пароль, блокировку аккаунта или изменившуюся страницу входа и при этом охотно генерировать трафик.

async function withSession(page, context, task) {
  if (!(await sessionIsAlive(context))) {
    await loginAndSaveState(context, page); // refresh file after login
  }
  try {
    return await task();
  } catch (error) {
    if (!(await sessionIsAlive(context))) {
      await loginAndSaveState(context, page); // one retry, then fail loudly
      return await task();
    }
    throw error;
  }
}

В фикстуре детектор увидел 401, повторный вход и проверка заняли 99 миллисекунд, и обновлённый файл состояния снова содержал ровно один сессионный cookie. Запуск закончился на защищённом дашборде с видимой таблицей и тремя отрисованными строками. Повторная аутентификация, которая заканчивается без этих трёх подтверждений, не завершена.

Если несколько workers делят один файл состояния, все могут заметить истечение в один момент и кинуться входить заново. Лекарство это координация single-flight: первый worker обновляет файл, остальные ждут, либо каждый worker обновляет свою копию. Механика та же, что у проблем сохранения сессии, которые возникают, когда несколько агентов работают против одного и того же профиля браузера.

Несколько аккаунтов и параллельные запуски

Изоляция действует на контекст, поэтому правило такое: один файл состояния на аккаунт или роль, один контекст на worker и никакой общей изменяемой сессии. В фикстуре два параллельных входа дали два разных сессионных cookie, оба зонда вернули 200 в одну миллисекунду, и ни один контекст не видел сессию другого. Это свойство нужно сохранять нарочно, потому что режим отказа, когда оно ломается, тонкий: два аккаунта перезаписывают состояние друг друга, и тест проходит за не того пользователя.

То же правило видно в настоящем браузере. Два Space это два контекста: один может простаивать на новой вкладке, второй держит сессию Airbnb после входа, не деля cookie так, как одно headed-окно. Смысл в изоляции. Обзор лишь показывает оба сразу.

Claude Code рядом с обзором Spaces в ego (lite): простой Google Space и запущенный Airbnb Space после входа
Простой Google Space рядом с запущенным Airbnb Space после входа. Изоляция видна; простаивающая вкладка это не второй поиск после входа.

Проверка, что аутентификация действительно сработала

Проверка это три проверки, и пропуск любой из них оставляет процесс тихо сломанным. Первая положительная: элемент, который существует только после входа, присутствует. Вторая отрицательная: формы входа нет, поэтому страница, на которой случайно есть и то и другое, не принимается. Третья это проверка данных, которая провалилась бы на устаревшей или кэшированной странице, например значение, которое должно меняться между запусками.

await page.waitForSelector("#revenue-table");      // positive
await expect(page.locator("#login-form")).toHaveCount(0); // negative
const probe = await context.request.get("/api/summary"); // data
assert.equal(probe.status(), 200);
assert.ok((await probe.json()).q3Revenue);

Все три прошли в запуске переиспользования фикстуры: таблица была видна, счётчик формы входа был ноль, и JSON-нагрузка содержала ожидаемое поле. Вместе они занимают несколько миллисекунд и превращают разницу между рабочей и сломанной сессией из умозаключения в факт. Больше приёмов отладки для запусков, которые всё ещё ведут себя плохо, описано в руководстве по отладке Playwright, а страница лучших практик разбирает соседние привычки, в том числе какие ожидания стоит писать.

Когда сессия принадлежит вашему настоящему браузеру

Файл storageState Playwright это то, чем вы владеете. Это правильный инструмент, когда аккаунт это фикстура, пароль лежит в секретах CI и у машины никто не сидит. Это неправильный инструмент, когда вход ваш: SSO, аппаратный ключ, push-запрос, сессия, которую вы уже держите тёплой в повседневном Chrome. В этом случае задача не восстановить вход. Задача взять взаймы браузер, в котором он уже есть.

ego (lite) и есть этот заём. Агент может открыть вкладку со входом, которой вы уже пользуетесь, продолжать работу в Space, который не крадёт мышь, и остановиться, когда второй фактор или подтверждение платежа требуют вас. Сессия никогда не становится JSON-файлом на диске. В этом смысл: личный вход должен оставаться в браузере, а не путешествовать по пути storageState, который CI потом случайно закоммитит.

Claude Code показывает завершённое сравнение объявлений Airbnb рядом с живым Space ego (lite) на airbnb.com.sg, видны Agent is in control и Take over
Сессия Airbnb после входа осталась в браузере. Агент работал в Space, где были видны Agent is in control и Take over.

Держите два инструмента раздельно. Безнадзорные тестовые аккаунты остаются на storageState, как руководство по аутентификации Playwright описывает. Личные дашборды остаются в видимом браузере. ego (lite) не нажимает MFA, не подтверждает платежи и не собирает учётные данные; эти остановки описаны в документации Space и в быстром старте. Текущая версия продукта 0.5.0.32 (changelog, 2026-09-12). Перепроверьте changelog и репозиторий GitHub, прежде чем цитировать более новую сборку.

FAQ

Почему Playwright всё равно оказывается на странице входа после сохранения storageState?

Playwright всё равно оказывается на странице входа после storageState, когда файл сохранили слишком рано, контекст его так и не загрузил или сервер отозвал cookie. Сначала проверьте cookies в файле, затем опции контекста. Если оба выглядят правильно, считайте это мёртвой сессией, а не ошибкой навигации.

Переживают ли сессионные cookies сохранение в storageState?

Да. Файл записывает cookies независимо от наличия срока действия, вместе с localStorage по origin. Не переживает то, что браузер хранит вне этой структуры: IndexedDB, session storage, кэш и service workers.

Как долго живёт сохранённый вход Playwright?

Пока сервер продолжает принимать сессию, а это решение сервера, которое он может отменить в любой момент. Дата истечения cookie это верхняя граница, а не обещание. Любой долгоживущий файл состояния считайте протухшим, пока зонд не скажет иное.

Могут ли параллельные workers делить один файл storageState?

Читать его они могут, и контекст каждого worker будет изолирован. Проблема появляется, когда один worker обновляет файл после повторного входа, а другой уже в середине запуска. Либо дайте каждому worker свою копию, либо сериализуйте обновление, чтобы вход происходил только один за раз.

Как пройти аутентификацию через OAuth или SSO в Playwright?

Один раз пройдите всю цепочку редиректов в контексте, который сохранит cookies провайдера идентификации, и сохраните состояние только после достижения итогового URL приложения. Если провайдер требует интерактивный шаг, сделайте его один раз вручную в том же запуске и потом переиспользуйте получившееся состояние.

Как обращаться с MFA и одноразовыми кодами?

Не автоматизируя второй фактор сохранённым кодом. Честный шаблон это разовый человеческий шаг, результатом которого становится сохранённая сессия, либо сессия, которая уже была аутентифицирована в вашем браузере. Хранение секретов OTP или SMS-кодов рядом с файлом состояния превращает средство защиты в обязательство.

Безопасно ли коммитить storageState в систему контроля версий?

Нет. В файле живые сессионные cookies. Держите его в рабочей директории, игнорируйте в git, а в CI подставляйте из хранилища секретов. Если он когда-либо был закоммичен, считайте сессию скомпрометированной и отзовите её.

Как намеренно проверить, что автоматизация обрабатывает истёкшую сессию?

Добавьте тестовый хук, который инвалидирует сессию на стороне сервера, как это делает эндпоинт revoke в фикстуре, и прогоните против него путь переиспользования. Проверьте редирект, 401 от зонда и восстановление. Этот один тест покрывает путь кода, который иначе срабатывает только в продакшене.

Что если приложение хранит токен в IndexedDB?

storageState его не унесёт, поэтому используйте один из двух путей: захватите токен скриптом страницы после входа и заново внедрите при переиспользовании, либо аутентифицируйтесь внутри каждого запуска через API, который выдаёт токен. Первый быстрее, второй слабее связан с внутренностями приложения.

Нужно ли на каждом запуске заново проходить аутентификацию вместо переиспользования состояния?

Для CI с одноразовым тестовым аккаунтом повторная аутентификация на каждый запуск безопаснее как значение по умолчанию. Для задания, которое часто работает против стабильной сессии, переиспользование плюс зонд и ограниченный повторный вход дешевле и проще для рассуждения. Оба варианта законны; непроверенное переиспользование нет.

Почему вход работает локально, а в CI падает?

Обычно потому, что файл состояния в CI отсутствует или старше, часы расходятся, или страница входа отдаёт другой вариант свежему IP. Воспроизведите с тем же headless, viewport и файлом состояния и прозондируйте сессию до запуска набора. Сбой часто средовой, а не разница в коде.

Может ли один файл storageState обслуживать два аккаунта?

Нет, а попытки слить их обычно оставляют тот cookie, который записали последним. Используйте один файл на аккаунт или роль, назовите его по личности, которой он принадлежит, и передавайте нужный каждому контексту.