Перейти к содержимому

5. JWT vs Sessions

ХарактеристикаSessionsJWT
Хранение состоянияСервер (БД/Redis)Клиент (токен)
МасштабированиеНужен shared storageЛёгкое
ОтзывМгновенныйСложный
Нагрузка на БДЕсть при каждом запросеНет (stateless)
РазмерМаленький cookie (32 байта)Больше (300-800 байт)
БезопасностьПроще управлятьНужна осторожность
  • Сервер рендерит 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 удобнее.

// React Native / Flutter
const 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 при каждом запросе!

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 используй 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.