Ответ
Наиболее безопасный способ — использование HttpOnly, Secure, SameSite cookies. Это защищает от атак межсайтового скриптинга (XSS) и межсайтовой подделки запросов (CSRF).
Пример установки защищённой куки на бэкенде (Node.js/Express):
res.cookie('auth_token', signedJwtToken, {
httpOnly: true, // Недоступна из JavaScript
secure: true, // Передаётся только по HTTPS
sameSite: 'strict', // Защита от CSRF
maxAge: 24 * 60 * 60 * 1000 // 1 день
});
Почему не LocalStorage/SessionStorage?
- Они доступны через JavaScript, что делает токен уязвимым при XSS-атаке.
- Злоумышленный скрипт может прочитать токен и отправить его на свой сервер.
Современный подход для SPA (Single Page Application):
- Access Token: Короткоживущий (минуты), хранится в памяти JavaScript (переменная, замыкание). При обновлении страницы теряется, и приложение запрашивает новый через специальный endpoint.
- Refresh Token: Долгоживущий, хранится строго в HttpOnly куке. Используется для получения новой пары access/refresh токенов.
Пример потока:
- Пользователь логинится, бэкенд устанавливает
refresh_tokenв HttpOnly куку и возвращаетaccess_tokenв теле ответа. - SPA сохраняет
access_tokenв памяти и использует для API-запросов. - Когда
access_tokenистекает, SPA отправляет запрос на/refresh(кукаrefresh_tokenотправится автоматически). - Бэкенд валидирует
refresh_tokenи выдаёт новую пару.
Этот подход минимизирует риски утечки долгоживущих учётных данных.
Ответ 18+ 🔞
Давай разберём эту тему, а то народ часто путается, как будто на дворе 2002-й год и все хранят токены в localStorage, а потом удивляются, почему их аккаунты уплыли к левым ребятам.
Самый надёжный вариант — это HttpOnly, Secure, SameSite куки. Представь, что это как сейф, который ты отдаёшь браузеру на хранение, а ключ от него только у сервера. JavaScript доступа к нему не имеет вообще — вот и вся защита от XSS. А SameSite не даёт куке улететь с твоим сайта на левые домены, что убивает большинство CSRF-атак на корню.
Вот как это на бэкенде выглядит (Node.js/Express):
res.cookie('auth_token', signedJwtToken, {
httpOnly: true, // Недоступна из JavaScript — никакой ерунды!
secure: true, // Только по HTTPS, чтоб не светить в открытую
sameSite: 'strict', // Жёсткая защита от CSRF
maxAge: 24 * 60 * 60 * 1000 // Живёт сутки
});
А теперь про LocalStorage/SessionStorage. Ребята, это же пиздец, а не хранилище для токенов! Это как оставить ключи от квартиры на видном месте в подъезде. Любой скрипт, который просочится на твою страницу (а такое бывает овердохуища часто), спокойно вычитает твой токен и отправит его куда надо. Доверия к этому подходу — ноль ебать.
Современный, умный подход для SPA (того самого одностраничного приложения):
- Access Token (Токен доступа): Живёт недолго, минут 15-30. Хранится только в памяти JavaScript — в переменной, в замыкании. Обновил страницу — токен испарился, и всё. Это как пропуск на день, который сгорает вечером.
- Refresh Token (Токен обновления): Вот это уже серьёзная вещь. Долгоживущий, хранится строго в той самой HttpOnly куке, которую мы описали выше. Его задача одна — выпросить у сервера новую пару токенов, когда access'у пришёл пиздец.
Как это работает на практике, чтоб было понятно:
- Юзер логинится. Бэкенд в ответе ставит
refresh_tokenв защищённую куку, а в теле JSON'а отдаётaccess_token. - Твое SPA хватает этот
access_token, пихает его в память и с ним ходит по всем API. - Внезапно время вышло, токен протух. Вместо того чтобы посылать юзера на повторный логин, приложение тихонько стучится на специальный эндпоинт
/refresh. Браузер автоматом цепляет к запросу ту самую HttpOnly куку сrefresh_token— тебе даже палец о палец ударить не надо. - Бэкенд проверяет refresh-токен, и если всё чисто — выдаёт свежую парочку: новый access (в JSON) и новый refresh (в куке).
Вот такая хитрая жопа. Риск утечки долгоживущих данных сводится почти к нулю, потому что самый ценный refresh-токен JavaScript'у не виден вообще. А короткоживущий access-токен, даже если его и скомпрометируют, быстро сдохнет. Красота, ёпта.