Backend

Как безопасно хранить access и refresh токены в браузере в 2026 году

Как безопасно хранить access и refresh токены в браузере в 2026 году

 

Вопрос хранения токенов в браузере — это вечный спор в сообществе фронтенд-разработчиков. Одни топят за localStorage, другие — за HttpOnly cookies, третьи предлагают хранить всё в памяти приложения. Проблема в том, что идеального, абсолютно безопасного места не существует. Любое решение — это всегда компромисс между удобством разработки (DX), пользовательским опытом (UX) и уровнем защищенности.

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

Анатомия проблемы: XSS против CSRF

Прежде чем выбирать «хранилище», нужно понять, от чего мы защищаемся. В веб-безопасности есть два главных врага:

  1. XSS (Cross-Site Scripting) — когда злоумышленник внедряет свой JS-код на вашу страницу. Если ваш токен лежит в localStorage или обычном cookie, скрипт просто прочитает его через document.cookie или localStorage.getItem() и отправит на сторонний сервер.
  2. CSRF (Cross-Site Request Forgery) — когда вредоносный сайт заставляет браузер пользователя отправить запрос на ваш сервер. Если вы используете куки, браузер прикрепит их автоматически, и сервер примет запрос за легитимный.

Главный парадокс в том, что защита от XSS (перенос токенов в HttpOnly куки) делает нас уязвимыми для CSRF. И наоборот.

Почему localStorage — это «смертный грех» для Access-токенов

Многие туториалы до сих пор учат складывать токены в localStorage. С точки зрения разработчика это удобно: один вызов API, сохранили строку, достали её при каждом запросе. Но с точки зрения безопасности — это катастрофа.

localStorage доступен любому JavaScript-коду, запущенному в контексте вашего домена. Это значит, что любая скомпрометированная библиотека из вашего node_modules или сторонний рекламный скрипт могут мгновенно украсть ваш Access-токен. Если срок жизни токена большой, злоумышленник получит полный доступ к аккаунту пользователя на долгое время.

Современная стратегия: Разделение ответственности

В 2026 году стандартом индустрии является использование связки из короткоживущего Access Token и долгоживущего Refresh Token. Но секрет не в самих токенах, а в том, как и где они живут.

1. Access Token: Хранение в памяти (In-Memory)

Самый безопасный способ хранения Access-токена — это обычная переменная в состоянии вашего приложения (например, в Redux, Vuex, или просто в замыкании сервиса аутентификации).

Почему это работает:

    • Переменная в памяти недоступна для XSS-атак через чтение хранилища.
    • При обновлении страницы токен стирается, что кажется минусом, но именно это делает его безопасным.

Как реализовать процесс:

    1. После успешного логина сервер присылает Access-токен в теле ответа (JSON).
    1. Фронтенд сохраняет его в переменную (state).
    1. Для каждого запроса токен добавляется в заголовок Authorization: Bearer <token>.
    1. При перезагрузке страницы приложение делает запрос на /refresh, чтобы получить новый Access-токен.

2. Refresh Token: Защищенные куки (HttpOnly, Secure, SameSite)

Поскольку Access-токен живет коротко (например, 15 минут), нам нужен механизм его обновления без повторного ввода пароля. Для этого используется Refresh-токен. Его нельзя хранить в памяти (он пропадет при перезагрузке) и нельзя в localStorage (его украдут).

Единственный приемлемый вариант — HttpOnly Cookie.

Настройки куки, которые должны быть обязательно:

    • HttpOnly: JavaScript не может прочитать эту куку. Это полностью отсекает XSS-кражу.
    • Secure: Кука передается только по HTTPS.
    • SameSite=Strict (или Lax): Запрещает отправку куки при кросс-доменных запросах, что практически полностью нивелирует риск CSRF.

Архитектура «Безопасного обновления» (The Refresh Flow)

Вот как выглядит идеальный цикл жизни сессии в современном приложении:

  1. Авторизация: Пользователь вводит логин/пароль $\rightarrow$ Сервер проверяет данные $\rightarrow$ Возвращает Access-токен в JSON-ответе и устанавливает Refresh-токен в Set-Cookie с флагами HttpOnly; Secure; SameSite=Strict.
  2. Работа: Приложение использует Access-токен из памяти для запросов.
  3. Истечение: Через 15 минут сервер возвращает 401 Unauthorized.
  4. Обновление: Фронтенд перехватывает 401 ошибку (через интерцепторы axios или fetch) и делает запрос на /refresh.
  5. Ротация: Сервер видит Refresh-токен в куках, проверяет его, генерирует новую пару (новый Access и новый Refresh) и возвращает их.
  6. Безопасность: Старый Refresh-токен аннулируется (это называется Refresh Token Rotation). Если злоумышленник всё же украл Refresh-токен и попытался его использовать, сервер заметит повторное использование старого токена и мгновенно заблокирует всю сессию пользователя.

Продвинутые методы защиты: BFF (Backend For Frontend)

Если вы строите высоконагруженное или корпоративное приложение с повышенными требованиями к безопасности, рассмотрите паттерн BFF.

Суть в том, что браузер вообще не видит никаких JWT. Вместо этого:

  1. Создается промежуточный сервер (BFF), который общается с основным API.
  2. BFF создает традиционную сессию с использованием зашифрованной куки.
  3. Все токены (Access и Refresh) хранятся на стороне BFF в защищенном хранилище или кэше (например, в Redis).
  4. Браузер общается с BFF через сессионную куку, а BFF «подмешивает» нужные токены перед отправкой запроса в основной микросервис.

Это полностью убирает риск утечки JWT с клиента, так как токены физически не покидают пределы серверного периметра.

Чек-лист для разработчика (Резюме)

Чтобы ваше приложение не стало легкой мишенью, проверьте выполнение следующих пунктов:

  1. [ ] Access-токен НЕ хранится в localStorage или sessionStorage.
  2. [ ] Refresh-токен хранится в куках с флагом HttpOnly.
  3. [ ] Установлен флаг SameSite=Strict для предотвращения CSRF.
  4. [ ] Реализована ротация Refresh-токенов (каждый Refresh используется один раз).
  5. [ ] Срок жизни Access-токена минимален (от 5 до 15 минут).
  6. [ ] Реализован механизм Logout, который удаляет куку на сервере и очищает память на клиенте.
  7. [ ] Используется Content Security Policy (CSP) для минимизации рисков XSS.

Заключение

Безопасность — это не конечная точка, а процесс. В 2024 году мы уходим от простых решений в сторону многослойной защиты. Хранение Access-токена в памяти в сочетании с ротируемыми HttpOnly куками для обновления — это золотой стандарт для большинства SPA (React, Vue, Angular).

Помните: чем меньше данных вы доверяете браузеру, тем спокойнее спите по ночам. Если ваш проект критически важен — переходите на архитектуру BFF. Если же вы делаете обычный сервис — следуйте схеме «Память + HttpOnly Cookies», и вы будете защищены от 99% типичных атак.