Back

Cookies vs localStorage для JWT-аутентификации

Cookies vs localStorage для JWT-аутентификации

Вы реализовали процесс аутентификации, JWT работают, и теперь вы смотрите на тот же вопрос, с которым рано или поздно сталкивается каждый frontend-разработчик: куда на самом деле положить этот токен? Ответ важнее, чем большинство руководств дают понять, а расхожий совет — «просто используйте HttpOnly cookies» — обходит стороной реальные компромиссы, которые необходимо понимать.

Ниже приведён чёткий разбор обоих вариантов: от чего каждый из них реально защищает и как современные приложения решают эту задачу на практике.

Ключевые выводы

  • localStorage полностью доступен любому JavaScript-коду, выполняющемуся на вашей странице, что делает его уязвимым к краже токенов через XSS.
  • HttpOnly-куки полностью блокируют доступ из JavaScript, однако создают риск CSRF, который снижается с помощью атрибутов SameSite и Secure.
  • Современный подход предполагает хранение краткосрочных токенов доступа в памяти, а токенов обновления — в HttpOnly, Secure, SameSite-куках.
  • Рекомендации OWASP и OAuth для браузерных приложений не рекомендуют размещать долгоживущие токены в localStorage.
  • Правильный выбор зависит от вашей модели угроз, возможности управлять бэкендом и того, требует ли ваш API заголовков Authorization.

Что реально поставлено на карту при хранении JWT

Место хранения JWT определяет, какие векторы атак применимы к вашему приложению. Два основных вида угроз:

  • XSS (межсайтовый скриптинг): вредоносный JavaScript, выполняющийся в контексте вашего приложения.
  • CSRF (межсайтовая подделка запросов): принуждение браузера пользователя к выполнению непреднамеренных аутентифицированных запросов.

Ни один из вариантов хранения не устраняет оба риска одновременно. Цель — понять, какой риск вы принимаете, и знать, как его снизить.

localStorage: удобно, но доступно из JavaScript

Хранить JWT в localStorage просто. Вы записываете токен, читаете его и вручную добавляете в заголовки Authorization: Bearer. Это хорошо работает с API, ожидающими именно такой формат заголовка.

Проблема в том, что localStorage полностью доступен любому JavaScript-коду на вашей странице. Если злоумышленнику удастся внедрить скрипт — через уязвимость в зависимости, скомпрометированный CDN или XSS-уязвимость в вашем собственном коде — он сможет напрямую прочитать токен и похитить его. Именно по этой причине OWASP явно не рекомендует хранить идентификаторы сессий в localStorage.

Это не теоретическая угроза. Современные веб-приложения подключают десятки сторонних скриптов, и каждый из них — потенциальная поверхность атаки.

HttpOnly-куки: лучшая защита от XSS, но новые нюансы

HttpOnly-куки вообще не читаются из JavaScript. Даже если злоумышленник выполняет код на вашей странице, он не может извлечь значение токена. Это существенное улучшение.

Однако куки создают уязвимость к CSRF. Браузеры автоматически прикрепляют куки к подходящим запросам, включая те, что инициированы вредоносными сторонними сайтами.

Три атрибута кук совместно закрывают эту брешь:

  • HttpOnly — полностью блокирует доступ из JavaScript.
  • Secure — передаёт куки только по HTTPS.
  • SameSite — управляет тем, когда куки отправляются при межсайтовых запросах.

Для SameSite: современные браузеры по умолчанию используют значение Lax, если атрибут не задан явно. Это блокирует куки при межсайтовых подзапросах (например, POST-запросах с другого источника), но разрешает их при навигации верхнего уровня. Значение Strict более консервативно: оно запрещает отправку куки при любых межсайтовых запросах, включая навигацию верхнего уровня. Всегда задавайте этот атрибут явно, не полагаясь на умолчания браузера. Поддержка SameSite в современных браузерах отличная, и её можно проверить на Can I Use.

При правильно настроенном SameSite=Strict или Lax риск CSRF существенно снижается для большинства схем аутентификации в рамках одного домена. Для критичных эндпоинтов, изменяющих состояние, дополните защиту антиCSRF-токеном для эшелонированной обороны.

Подход, который используют большинство современных приложений

Многие production-приложения разделяют задачу на части:

  1. Краткосрочные токены доступа хранятся в памяти JavaScript (переменная уровня модуля или состояние React).
  2. Токены обновления хранятся в HttpOnly, Secure, SameSite-куке.

Токен доступа исчезает при закрытии вкладки или обновлении страницы, однако тихий вызов эндпоинта /refresh получает новый токен с использованием куки. Токен доступа никогда не попадает в постоянное хранилище, а токен обновления никогда не доступен из JavaScript.

Этот подход соответствует актуальным рекомендациям для браузерных приложений, использующих OAuth 2.0 с PKCE (Authorization Code Flow with PKCE) — именно это предписывает руководство OAuth 2.0 for Browser-Based Apps. Если вы работаете с OpenID Connect (OIDC), тот же подход применим и там — держите ID-токены и токены обновления подальше от localStorage.

Чеклист аудита безопасности

Перед выпуском убедитесь, что:

  • Флаг HttpOnly установлен на любой куке, хранящей токены.
  • Флаг Secure включён (HTTPS обязателен).
  • SameSite явно задан как Strict или Lax.
  • Токены доступа краткосрочны — как правило, их время жизни измеряется минутами, а не часами.
  • Настроены заголовки Content Security Policy.
  • Долгоживущие JWT не хранятся в localStorage.

Как выбрать подходящий подход для вашего приложения

Универсального ответа нет. Если вы управляете бэкендом и раздаёте приложение с того же домена, HttpOnly-куки с правильно настроенным SameSite — более надёжный выбор по умолчанию. Если вы интегрируетесь со сторонним API, требующим заголовков Authorization, и не можете устанавливать куки на стороне сервера, хранение в памяти с коротким сроком жизни — разумная альтернатива. Главное — никогда не сохраняйте долгоживущие токены в localStorage.

Заключение

Долгоживущие JWT в localStorage — именно то, от чего актуальные рекомендации по безопасности последовательно предостерегают. HttpOnly-куки с атрибутами Secure и SameSite обеспечивают наиболее надёжную защиту по умолчанию для большинства схем в рамках одного домена, тогда как хранение в памяти в сочетании с куки для токена обновления покрывает более сложные случаи. Как только вы разберётесь с моделью угроз — XSS с одной стороны, CSRF с другой — правильный выбор для вашего приложения превратится из догадки в осознанный компромисс.

Часто задаваемые вопросы

sessionStorage имеет ту же уязвимость, что и localStorage: любой JavaScript-код на странице может его прочитать. Единственное отличие — sessionStorage очищается при закрытии вкладки. Это сужает окно уязвимости, но не защищает от XSS. При хранении токенов относитесь к sessionStorage с той же осторожностью, что и к localStorage, и не размещайте там долгоживущие токены.

SameSite=Strict запрещает отправку куки при межсайтовых запросах, что блокирует большинство сценариев CSRF-атак. Тем не менее для критичных эндпоинтов, изменяющих состояние, добавление антиCSRF-токена обеспечивает эшелонированную оборону. SameSite применяется браузером, поэтому старые клиенты или нестандартные пограничные случаи могут его не соблюдать. Паттерн двойной отправки токена остаётся разумной мерой предосторожности.

Распространённый диапазон — от 5 до 15 минут. Достаточно короткий, чтобы украденный токен имел ограниченную ценность, но достаточно длинный, чтобы не перегружать эндпоинт обновления. Дополните это токеном обновления с более длинным сроком жизни (от нескольких часов до нескольких дней) в HttpOnly-куке. Если ваше приложение обрабатывает чувствительные операции, например платежи, склоняйтесь к более короткому сроку жизни и требуйте повторной аутентификации для критичных действий.

Храните токен доступа в памяти JavaScript — в переменной модуля, состоянии React или замыкании — вместо localStorage. Делайте его краткосрочным и по возможности обновляйте через эндпоинт бэкенда. Если вам необходимо что-то сохранять между перезагрузками, направьте процесс обновления через собственный бэкенд, который хранит долгоживущие учётные данные на стороне сервера, и никогда не передавайте долгоживущие токены в клиентское хранилище.

Gain control over your UX

See how users are using your site as if you were sitting next to them, learn and iterate faster with OpenReplay. — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay