Получили токен с сервера — куда его положить, чтобы потом доставать? Два главных варианта: куки и веб-хранилища (localStorage / sessionStorage). У обоих свои плюсы, минусы и сценарии атак. Разберём — и сделаем осознанный выбор, а не «куда первым научились, туда и пихаю».
localStorage / sessionStorage
Самое простое: вызов localStorage.setItem('token', '...'), и токен лежит в браузере до конца света (sessionStorage — до закрытия вкладки).
Плюсы:
- Тривиальный API, известен всем.
- JS легко достаёт токен и сам кладёт в Authorization-заголовок — полностью контролируемо.
- Никаких сюрпризов с CSRF (об этом ниже): браузер сам ничего не прикрепляет к запросам.
Минусы:
- Доступно из любого JS на странице. А значит — уязвимо к XSS: если злоумышленник внедрил скрипт (например, через комментарий или сторонний пакет в node_modules) — он мгновенно вытащит ваш токен и улетит с ним к себе на сервер.
- Сохраняется между вкладками и сессиями. Если устройство общее — токен живёт после ухода пользователя.
HttpOnly Secure куки
Сервер ставит куку в ответе:
Set-Cookie: refreshToken=fF8a2d...; HttpOnly; Secure; SameSite=Strict; Path=/api
Разбор флагов:
- HttpOnly — куку нельзя прочитать из JavaScript. Никакой document.cookie её не увидит. Браузер прикрепляет к запросам на тот же домен сам, JS не знает значения.
- Secure — кука уходит только по HTTPS. По обычному HTTP не передаётся.
- SameSite=Strict | Lax | None — правила прикрепления к запросам с других сайтов (защита от CSRF). Strict — не прикреплять никогда с других сайтов; Lax — только на «безопасные» навигации (клик по ссылке); None — прикреплять всегда (требует Secure).
- Path=/api — куку прикреплять только к запросам на пути /api и ниже.
Плюсы:
- XSS не может прочитать токен — HttpOnly его прячет от JS.
- JS не нужно вручную прикреплять токен — куки браузер сам подставляет.
Минусы:
- CSRF: если кука прикрепляется автоматически, злоумышленник может с другого сайта сделать запрос на ваш бэк — и кука улетит вместе с ним. Защита: SameSite=Strict/Lax + CSRF-токен в дополнительном заголовке.
- Работает только в рамках своего домена. Третьесторонние API через куки авторизовать сложно.
Что такое XSS и CSRF одной фразой каждый
- XSS (Cross-Site Scripting) — злоумышленник запустил свой JS на вашем сайте (например, через невычищенный пользовательский ввод). Может всё, что может ваш JS: прочитать localStorage, делать запросы от вашего имени, перехватывать клики.
- CSRF (Cross-Site Request Forgery) — злоумышленник со своего сайта обманом вызвал запрос на ваш бэк от имени пользователя. Браузер сам прикрепил куку. Сервер не знает, что запрос пришёл не из легитимного интерфейса.
HttpOnly-куки защищают от XSS-кражи токена, но открыты CSRF. localStorage защищён от CSRF (браузер ничего не прикрепляет сам), но уязвим к XSS.
Что выбрать на практике
Прагматичный современный паттерн:
- Refresh token — всегда в HttpOnly Secure SameSite=Strict куке. Это самый ценный токен (живёт неделями), его потеря катастрофична. XSS его украсть не может.
- Access token — в памяти JS (переменная в модуле, контекст React). При перезагрузке страницы пропадает — не страшно, сразу делаем refresh и получаем новый.
- В localStorage токены не кладём вообще — даже короткоживущий access. XSS его перехватит за секунду.
Если бэкенд не поддерживает refresh через куки и у вас нет выбора — access кладёте в sessionStorage (хотя бы умрёт с закрытием вкладки), и яростно защищаетесь от XSS на уровне фронта: CSP, sanitization, никаких innerHTML с пользовательским контентом.
SameSite=Strict защищает почти от всего CSRF
Раньше CSRF-токены были обязательной частью защиты — сервер ставил один-разовый токен в куку, фронт его читал и прикреплял в заголовок к каждому небезопасному запросу. Сервер сверял.
Сегодня SameSite=Strict закрывает большинство CSRF-сценариев сам. Кука не уйдёт ни с одного стороннего сайта. CSRF-токен остаётся как пояс с подтяжками, но в современных приложениях постепенно уходит.
Резюме
| Куда положить | XSS | CSRF | Когда |
| localStorage | уязвим | защищён | почти никогда — лучше избегать |
| sessionStorage | уязвим | защищён | fallback, если нет HttpOnly-куки |
| JS-переменная в памяти | уязвим | защищён | хорошее место для access token |
| HttpOnly Secure cookie | защищён | уязвим без SameSite | лучшее место для refresh token |