Проблема заключается в том, как сохранять и восстанавливать авторизованную сессию пользователя без повторной авторизации в автотестах.
Авторизация тяжёлая (сертификат, организация, редиректы), и хочется один раз авторизоваться, сохранить состояние браузера и потом переиспользовать его.
Я попытался сделать следующее:
- Сохранять 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?
- есть ли рабочие подходы?
- или это в принципе ограничение