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-токеном для эшелонированной обороны.
Discover how at OpenReplay.com.
Подход, который используют большинство современных приложений
Многие production-приложения разделяют задачу на части:
- Краткосрочные токены доступа хранятся в памяти JavaScript (переменная уровня модуля или состояние React).
- Токены обновления хранятся в
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.