12k
All articles

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

Cookies или localStorage для JWT-аутентификации: разбор рисков XSS и CSRF, HttpOnly, Secure, SameSite и современных схем хранения токенов.

OpenReplay Team
OpenReplay Team
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 для JWT?

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

Нужна ли мне CSRF-защита, если я использую куки с SameSite=Strict?

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

Каким должен быть срок жизни токена доступа?

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

Что делать, если мой API принимает только заголовки Authorization и я не могу использовать куки?

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

Open-source session replay

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.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.