Как сохранять и восстанавливать сессию пользователя без повторной авторизации?

Проблема заключается в том, как сохранять и восстанавливать авторизованную сессию пользователя без повторной авторизации в автотестах.

Авторизация тяжёлая (сертификат, организация, редиректы), и хочется один раз авторизоваться, сохранить состояние браузера и потом переиспользовать его.

Я попытался сделать следующее:

  • Сохранять cookies и восстанавливать их
  • Сохранять localStorage / sessionStorage
  • Сохранять текущий URL и открывать его после восстановления
  • Играться с очисткой/неочисткой cookie и кэша
  • Пробовал изоляцию через WebDriver BiDi (userContext) вместо восстановления сессии
  • Рассматривал вариант с user-data-dir (профиль браузера)

У меня получилось:

  • Частично восстановить сессию через cookies + storage
  • Через BiDi получить изолированные контексты пользователей и переключаться между ними
  • Через user-data-dir получить рабочую авторизацию без повторного логина (но без изоляции)

У меня не получилось:

  • Стабильно восстанавливать сессию только через cookies + storage
  • Избежать 401 после “восстановления”
  • Сделать так, чтобы авторизация корректно “переживала” восстановление
  • Использовать BiDi без проблем:
    • extension не работает в новых контекстах без incognito
    • нельзя нормально включить incognito доступ программно
    • проблемы с новыми вкладками
  • Найти “чистое” решение без профиля браузера

Код

package helpers.browserstate;

import com.codeborne.selenide.Selenide;
import org.openqa.selenium.Cookie;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URI;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import static com.codeborne.selenide.WebDriverRunner.getWebDriver;

public class BrowserStateManager {
    private static final Logger logger = LoggerFactory.getLogger(BrowserStateManager.class);

    public void saveCurrentState(String label) {
        WebDriver driver = getWebDriver();
        String pageUrl = driver.getCurrentUrl();
        String currentOrigin = extractOrigin(pageUrl);

        Map<String, OriginStorageState> storageByOrigin = new LinkedHashMap<>();
        storageByOrigin.put(currentOrigin, OriginStorageState.builder()
                .origin(currentOrigin)
                .localStorage(readStorage("localStorage"))
                .sessionStorage(readStorage("sessionStorage"))
                .build());

        BrowserStateSnapshot snapshot = BrowserStateSnapshot.builder()
                .label(label)
                .pageUrl(pageUrl)
                .cookies(driver.manage().getCookies().stream()
                        .map(StoredCookie::from)
                        .collect(Collectors.toList()))
                .storageByOrigin(storageByOrigin)
                .build();

        BrowserStateRepository.save(snapshot);

        logger.info("Сохранено состояние браузера по лейблу '{}'. pageUrl={}, cookies={}",
                label, pageUrl, snapshot.getCookies().size());
    }

    public void restoreState(String label) {
        BrowserStateSnapshot snapshot = BrowserStateRepository.get(label);
        WebDriver driver = getWebDriver();

        driver.manage().deleteAllCookies();
        clearStorage();

        restoreCookiesByDomain(snapshot.getCookies());
        restoreStorage(snapshot.getStorageByOrigin());

        Selenide.open(snapshot.getPageUrl());
        Selenide.refresh();

        logger.info("Восстановлено состояние браузера по лейблу '{}'. pageUrl={}",
                label, snapshot.getPageUrl());
    }

    private void restoreCookiesByDomain(List<StoredCookie> cookies) {
        Map<String, List<StoredCookie>> cookiesByDomain = cookies.stream()
                .filter(cookie -> cookie.getDomain() != null && !cookie.getDomain().isBlank())
                .collect(Collectors.groupingBy(cookie -> normalizeDomain(cookie.getDomain()), LinkedHashMap::new, Collectors.toList()));

        for (Map.Entry<String, List<StoredCookie>> entry : cookiesByDomain.entrySet()) {
            String domain = entry.getKey();
            List<StoredCookie> domainCookies = entry.getValue();

            String url = "https://" + domain;
            Selenide.open(url);

            for (StoredCookie storedCookie : domainCookies) {
                try {
                    Cookie cookie = storedCookie.toSeleniumCookie();
                    getWebDriver().manage().addCookie(cookie);
                    logger.info("Добавлена cookie '{}' для домена '{}'", storedCookie.getName(), storedCookie.getDomain());
                } catch (Exception e) {
                    logger.warn("Не удалось добавить cookie '{}' для домена '{}'. Текущий URL='{}'. Ошибка: {}",
                            storedCookie.getName(),
                            storedCookie.getDomain(),
                            getWebDriver().getCurrentUrl(),
                            e.getMessage());
                }
            }
        }
    }

    private void restoreStorage(Map<String, OriginStorageState> storageByOrigin) {
        if (storageByOrigin == null || storageByOrigin.isEmpty()) {
            return;
        }

        for (OriginStorageState state : storageByOrigin.values()) {
            Selenide.open(state.getOrigin());
            clearStorage();
            writeStorage("localStorage", state.getLocalStorage());
            writeStorage("sessionStorage", state.getSessionStorage());
        }
    }

    @SuppressWarnings("unchecked")
    private Map<String, String> readStorage(String storageName) {
        JavascriptExecutor js = (JavascriptExecutor) getWebDriver();

        Object raw = js.executeScript("""
                const storage = window[arguments[0]];
                const result = {};
                for (let i = 0; i < storage.length; i++) {
                    const key = storage.key(i);
                    result[key] = storage.getItem(key);
                }
                return result;
                """, storageName);

        if (raw == null) {
            return new LinkedHashMap<>();
        }

        return new LinkedHashMap<>((Map<String, String>) raw);
    }

    private void writeStorage(String storageName, Map<String, String> values) {
        JavascriptExecutor js = (JavascriptExecutor) getWebDriver();

        js.executeScript("""
                const storage = window[arguments[0]];
                const values = arguments[1] || {};
                Object.keys(values).forEach(key => storage.setItem(key, values[key]));
                """, storageName, values);
    }

    private void clearStorage() {
        JavascriptExecutor js = (JavascriptExecutor) getWebDriver();
        js.executeScript("window.localStorage.clear();");
        js.executeScript("window.sessionStorage.clear();");
    }

    private String extractOrigin(String url) {
        URI uri = URI.create(url);
        return uri.getScheme() + "://" + uri.getAuthority();
    }

    private String normalizeDomain(String domain) {
        return domain.startsWith(".") ? domain.substring(1) : domain;
    }
}

Логи :

Failed to complete negotiation with the server: Error: Unauthorized: Status code '401'
SignalR error: Пользователь не авторизован
  • Chromium-Gost (portable)
  • Selenium 4.x
  • Selenide 7.x
  • CryptoPro extension

Ищу советы:

  • кто как решает переиспользование сессий?
  • реально ли обойтись без user-data-dir?
  • есть ли рабочие подходы?
  • или это в принципе ограничение