Аутентификация: 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.

Проверьте себя
1. Что гарантирует подпись JWT?
AЧто содержимое токена зашифровано и недоступно для чтения
BЧто содержимое не изменяли; при этом payload читается кем угодно
CЧто токен нельзя переиспользовать дважды
DЧто токен автоматически истекает через 15 минут
2. Почему access-токен JWT делают короткоживущим?
AЧтобы уменьшить размер заголовка Authorization
BПотому что отозвать выданный токен нельзя, и короткий срок ограничивает окно злоупотребления
CПотому что этого требует спецификация OAuth 2.0 для всех токенов
DЧтобы снизить нагрузку на Redis
3. Чем плохо хранение сессий в MemoryStore при нескольких процессах приложения?
AMemoryStore не поддерживает httpOnly-cookie
BСессия лежит в памяти конкретного процесса, поэтому на другом инстансе пользователь окажется неавторизованным
CMemoryStore шифрует данные и работает слишком медленно
DНичем, это рекомендованный вариант для продакшена