5. JWT vs Sessions
Сравнение концепций
Заголовок раздела «Сравнение концепций»| Характеристика | Sessions | JWT |
|---|---|---|
| Хранение состояния | Сервер (БД/Redis) | Клиент (токен) |
| Масштабирование | Нужен shared storage | Лёгкое |
| Отзыв | Мгновенный | Сложный |
| Нагрузка на БД | Есть при каждом запросе | Нет (stateless) |
| Размер | Маленький cookie (32 байта) | Больше (300-800 байт) |
| Безопасность | Проще управлять | Нужна осторожность |
Когда использовать Sessions
Заголовок раздела «Когда использовать Sessions»Традиционные веб-приложения
Заголовок раздела «Традиционные веб-приложения»- Сервер рендерит HTML (SSR)
- Пользователи работают в браузере
- Нет мобильных клиентов
// express-session — простота и надёжностьapp.use(session({ store: new RedisStore({ client }), secret: process.env.SESSION_SECRET, cookie: { httpOnly: true, secure: true },}));
app.post('/login', async (req, res) => { const user = await authenticate(req.body); req.session.userId = user.id; res.redirect('/dashboard');});Когда нужен мгновенный отзыв
Заголовок раздела «Когда нужен мгновенный отзыв»// Принудительный logout всех сессий пользователяasync function logoutAllSessions(userId) { // Удаляем все сессии пользователя из Redis const keys = await redis.keys(`sess:*`); for (const key of keys) { const session = await redis.get(key); const parsed = JSON.parse(session); if (parsed.userId === userId) { await redis.del(key); } }}Когда хранишь много данных о сессии
Заголовок раздела «Когда хранишь много данных о сессии»Сессия хранится на сервере — можно хранить много данных без увеличения нагрузки на сеть.
Когда использовать JWT
Заголовок раздела «Когда использовать JWT»API для мобильных приложений
Заголовок раздела «API для мобильных приложений»Мобильные приложения не используют куки нативно. JWT удобнее.
// React Native / Flutterconst response = await fetch('/api/login', { method: 'POST', body: JSON.stringify({ email, password }),});const { accessToken } = await response.json();
// Сохраняем в secure storage (не localStorage!)await SecureStore.setItemAsync('token', accessToken);
// Используем в запросахconst apiResponse = await fetch('/api/user', { headers: { 'Authorization': `Bearer ${accessToken}` },});Микросервисная архитектура
Заголовок раздела «Микросервисная архитектура»JWT можно верифицировать без обращения к центральному серверу:
Auth Service │ ┌───────────┼───────────┐ ▼ ▼ ▼ User Service Order Service Pay Service
Каждый сервис верифицирует JWT своим публичным ключом.Не нужно ходить в Auth Service при каждом запросе!Temporary access links
Заголовок раздела «Temporary access links»JWT идеальны для одноразовых ссылок:
function createPasswordResetToken(userId) { return jwt.sign( { sub: userId, purpose: 'password-reset' }, process.env.JWT_SECRET, { expiresIn: '1h', jti: crypto.randomUUID() } );}
// Ссылка: /reset-password?token=eyJ...app.post('/reset-password', async (req, res) => { const { token, newPassword } = req.body;
const payload = jwt.verify(token, process.env.JWT_SECRET);
if (payload.purpose !== 'password-reset') { return res.status(400).json({ error: 'Invalid token purpose' }); }
await changePassword(payload.sub, newPassword); res.json({ success: true });});Гибридный подход (рекомендуется)
Заголовок раздела «Гибридный подход (рекомендуется)»Refresh token в httpOnly cookie (как сессия) + Access JWT в памяти
Это лучшее из двух миров:
┌─────────────────────────────────────────────────────────┐│ Клиент ││ ││ Memory: accessToken (15 min) ││ httpOnly Cookie: refreshToken (30 days) │└──────────────────────────────────┬──────────────────────┘ │ ┌──────────────┼──────────────────┐ ▼ ▼ Access token Refresh token (15 мин, stateless) (30 дней, хранится в БД) верифицируется быстро можно отозвать мгновенно// Клиент: хранение в памяти (не localStorage!)let accessToken = null;
async function getAccessToken() { if (accessToken && !isExpired(accessToken)) { return accessToken; }
// Запрашиваем новый через refresh token (из cookie) const response = await fetch('/api/refresh', { method: 'POST', credentials: 'include', // отправляет cookie });
const data = await response.json(); accessToken = data.accessToken; return accessToken;}
// Axios interceptor для авто-обновления токенаaxios.interceptors.response.use( response => response, async error => { if (error.response?.status === 401) { const token = await getAccessToken(); error.config.headers['Authorization'] = `Bearer ${token}`; return axios.request(error.config); } return Promise.reject(error); });Next.js: рекомендуемый подход
Заголовок раздела «Next.js: рекомендуемый подход»В Next.js используй NextAuth.js с сессиями через JWT:
// Гибрид: JWT внутри, session cookie снаружиexport const authOptions = { providers: [/* ... */], session: { strategy: 'jwt', // JWT хранится в зашифрованном cookie maxAge: 30 * 24 * 60 * 60, // 30 дней }, callbacks: { jwt({ token, user }) { if (user) { token.role = user.role; } return token; }, session({ session, token }) { session.user.role = token.role; return session; }, },};Таблица решений
Заголовок раздела «Таблица решений»Твой сценарий Рекомендация────────────────────────────────────────────────Традиционный SSR сайт Sessions (Redis)SPA + один backend JWT в cookieМобильное приложение JWT (access + refresh)Микросервисы JWT (RS256)Third-party OAuth NextAuth.js / PassportВременные ссылки Short-lived JWTВысокая безопасность Sessions + 2FAСессии надёжнее и проще в управлении. Выбирай для традиционных веб-приложений.
JWT гибче и масштабируется лучше. Выбирай для API, мобильных приложений и микросервисов.
Гибрид (refresh cookie + access JWT в памяти) — лучший баланс для большинства SPA.