Аутентификация: JWT против сессий
«JWT или сессии — что выберете и почему?» Правильного ответа тут нет, проверяется умение сравнивать компромиссы.
JWT — это подписанный, но не зашифрованный токен: любой, у кого он есть, может прочитать payload. Подпись гарантирует только то, что содержимое не изменяли.
Что проверяет интервьюер
Понимаете ли вы разницу между stateless и stateful; знаете ли, что в JWT нельзя класть секреты; и главное — осознаёте ли проблему отзыва. Ответ «JWT лучше, потому что современнее» закрывает вопрос не в вашу пользу.
Как устроен JWT
// три части, разделённые точкой: header.payload.signature
// eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMiLCJyb2xlIjoidXNlciJ9.qDT8...
const jwt = require('jsonwebtoken');
const token = jwt.sign(
{ sub: user.id, role: user.role }, // payload: только несекретные данные
process.env.JWT_SECRET,
{ expiresIn: '15m' },
);
try {
const payload = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256'], // алгоритм задаём явно
});
req.user = { id: payload.sub, role: payload.role };
} catch (err) {
throw new AppError(401, 'invalid_token', 'Токен недействителен');
}
Header и payload — это обычный base64url, декодируется в любой консоли за секунду. Поэтому в токен не кладут пароли, персональные данные и внутренние идентификаторы, которые не хотелось бы светить. Явное указание algorithms в verify — не педантизм, а защита от двух известных атак: подмены на alg: none и подстановки HS256 вместо RS256 с публичным ключом в роли секрета.
Сессии
const session = require('express-session');
const RedisStore = require('connect-redis').default;
app.use(session({
store: new RedisStore({ client: redis }), // не MemoryStore: она не переживёт рестарт
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true, // JavaScript на странице не прочитает
secure: true, // только по HTTPS
sameSite: 'lax', // базовая защита от CSRF
maxAge: 86400000,
},
}));
Клиенту уходит только идентификатор сессии, все данные лежат на сервере. Хранилище обязано быть общим (Redis, база): MemoryStore по умолчанию годится лишь для разработки — при нескольких процессах пользователь будет случайно «разлогиниваться», попадая на другой инстанс.
Сравнение
| Критерий | JWT | Сессии |
| Состояние на сервере | нет | есть, нужно хранилище |
| Проверка | подпись, без похода в базу | чтение из Redis/БД |
| Отзыв доступа | сложно: токен валиден до истечения | мгновенно: удалили запись |
| Смена роли или прав | подхватится только после обновления токена | сразу |
| Размер на запрос | сотни байт | десятки байт |
| Где уместнее | API, мобильные клиенты, межсервисные вызовы | классические веб-приложения, требования к мгновенному logout |
Главная проблема JWT и как её решают
Токен нельзя «выключить»: сервер не хранит состояние и проверяет только подпись и срок. Если токен украли или пользователя уволили, он останется валидным до exp. Стандартная схема-компромисс:
- Access-токен живёт коротко — 5–15 минут. Окно злоупотребления мало.
- Refresh-токен — длинный, хранится на сервере и потому отзываем; по нему выдаётся новый access.
- Ротация refresh-токенов: при каждом обновлении старый инвалидируется. Повторное использование старого — сигнал кражи, вся цепочка сессий гасится.
- Denylist для экстренного отзыва по
jti. Заметьте: это уже состояние на сервере, то есть частичный отказ от главного преимущества JWT.
Где хранить токен на клиенте
Ещё один любимый вопрос. В localStorage удобно, но любая XSS-уязвимость — и токен утёк, потому что JavaScript его читает. Cookie с флагами httpOnly, secure, sameSite недоступна скриптам, зато браузер отправляет её автоматически, что открывает CSRF — его закрывают sameSite и CSRF-токеном. Сильный ответ звучит так: «для веба — httpOnly-cookie плюс защита от CSRF; для мобильных клиентов — безопасное хранилище платформы и заголовок Authorization».
Типичные ошибки кандидатов
- Считают, что JWT зашифрован и туда можно класть чувствительные данные.
- Не знают про невозможность отзыва и не могут предложить схему с refresh-токенами.
- Выдают access-токен на 30 дней «чтобы пользователь не разлогинивался».
- Хранят сессии в
MemoryStoreи не понимают, почему при масштабировании ломается авторизация. - Отвечают «JWT всегда лучше», не разбирая компромиссы.
Как ответить кратко
Сессия — состояние на сервере, клиенту уходит только идентификатор: отозвать доступ можно мгновенно, но нужно общее хранилище вроде Redis. JWT — подписанный самодостаточный токен, проверяется без похода в базу и потому удобен для API и межсервисных вызовов, но он не зашифрован и его нельзя отозвать до истечения срока. Отсюда практика: короткий access-токен на минуты плюс отзываемый refresh с ротацией. Хранить в вебе лучше в httpOnly-cookie с
sameSiteи защитой от CSRF, а не вlocalStorage, который доступен любому XSS.