ГлавнаяБлогСессии или JWT: что выбрать для авторизации в 2025
Алгоритмы

Сессии или JWT: что выбрать для авторизации в 2025

Разбираем сессии и JWT: где хранится правда, как работает отзыв доступа и что выбрать для вашего API. Практические советы и код.

Al
Редакция Algolitalgolit.ru
9 мин чтения1 сентября 2026 г.

Каждое приложение на каждый запрос отвечает на один и тот же вопрос: «Кто это и можно ли ему это делать?». Два популярных ответа — сессии и JWT. Вокруг них много хайпа, но выбор часто делают неправильно. В этой статье разберем, чем они отличаются на самом деле, и как не наступить на грабли, которые ломают продакшн.

Сессии: модель гардероба

Вы логинитесь. Сервер проверяет пароль и, если всё ок, записывает строку в хранилище. В этой строке — ваш user id, срок действия, возможно роли. Хранится она в Redis, Postgres или в памяти, если вы смелые. Сервер отправляет вам cookie с одним-единственным значением — случайным id. Это не ваша личность. Это номерок из гардероба.

На каждом следующем запросе сервер берёт ваш session id, идёт в хранилище и спрашивает: «Кто это у нас?». Ваша личность никогда не лежит в cookie. Она подтягивается свежей каждый раз. Это даёт важное следствие: сервер может мгновенно передумать о вас. Удалил строку — и следующий же запрос с этим cookie приходит как от незнакомца. Забанить пользователя, принудительно разлогинить, отозвать скомпрометированную сессию — всё это один DELETE.

JWT: модель подписанной записки

Тот же логин, та же проверка пароля. Но вместо записи в хранилище сервер собирает JSON-объект, подписывает его и отдаёт вам целиком. Токен состоит из трёх частей, разделённых точками: заголовок, полезная нагрузка, подпись. Спецификация — RFC 7519.

Самая важная вещь о полезной нагрузке, которую многие упускают в продакшене:

# возьмите среднюю часть любого JWT и просто прочитайте её
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq
{
  "sub": "user_8823",
  "email": "maneshwar@example.com",
  "role": "admin",
  "exp": 1735689600
}

Никакого ключа, никакого пароля — просто base64. JWT подписан, а не зашифрован. Любой, у кого есть токен, может прочитать все утверждения внутри. jwt.io сделает это в браузере. Подпись не скрывает содержимое, она лишь доказывает, что содержимое не меняли после подписи. Поэтому никогда не кладите в payload то, что не напечатали бы на открытке: никаких секретов, внутренних флагов, «isTrialAbuser».

Главное отличие: где живёт правда

Забудьте про аббревиатуры. С сессиями правда живёт на вашем сервере, а клиент держит указатель на неё. С JWT правда живёт в кармане клиента, а сервер имеет способ проверить почерк. Всё остальное вытекает из этого.

Добавьте второй сервер — и увидите разницу. Сессиям нужно, чтобы каждый сервер имел доступ к одному хранилищу, а это сетевой хоп на каждый запрос и ещё одна точка отказа. JWT не требуют ничего общего: у каждого сервера есть ключ, каждый проверяет локально, добавить четвёртый сервер — не событие. Это действительно здорово, и именно поэтому JWT захватили микросервисы. Но посмотрите на нижнюю часть обеих колонок — там, где вы платите.

Часть, которую никто не показывает на слайдах

Вот вопрос, который решает всё: что происходит между моментом, когда вы решили, что пользователь должен быть разлогинен, и моментом, когда это реально происходит?

Для сессии этот разрыв — один запрос. Вы удаляете строку, следующий запрос падает, готово. Для простого JWT этот разрыв — сколько осталось на часах. Вы можете удалить пользователя из базы, отключить аккаунт, отозвать API-ключи, поджечь здание — токен всё равно работает. Каждый сервер, который его увидит, радостно проверит подпись, найдёт её валидной и обслужит запрос. Это не баг. Это дизайн. Statelessность означает, что никто ни с кем не сверяется, а информация «этот пользователь забанен» живёт где-то ещё.

Изобретение refresh-токенов и что из этого вышло

Стандартное решение известно: сделать access-токен короткоживущим — около 15 минут — и добавить долгоживущий refresh-токен. Когда access истекает, клиент тихо обменивает refresh на новый. Пользователь ничего не замечает, окно кражи сокращается с дней до минут. Это работает, и вы должны так делать. Но посмотрите на refresh-эндпоинт:

app.post('/auth/refresh', async (req, res) => {
  const { refreshToken } = req.body;

  // вот оно
  const stored = await redis.get(`refresh:${refreshToken}`);
  if (!stored) return res.sendStatus(401); // отозван или не существовал

  const { userId } = JSON.parse(stored);
  const user = await db.users.findById(userId);
  if (user.disabled) return res.sendStatus(401); // забанен с прошлого refresh

  return res.json({ accessToken: signAccessToken(user) });
});

Посчитайте, что тут есть: поиск в хранилище, проверка отзыва, поход в базу, чтобы проверить, что пользователь ещё допущен. Это сессия. Вы написали сессию. Refresh-токен — это непрозрачный id, указывающий на серверное состояние, которое можно удалить в любой момент, — точное определение того, от чего мы якобы ушли. Разница лишь в том, что вы проверяете это раз в 15 минут вместо каждого запроса.

И это настоящий ответ: вы не выбираете между stateful и stateless. Вы выбираете, как часто готовы платить за состояние и как долго готовы терпеть ошибку между проверками. Сессии платят на каждом запросе и никогда не ошибаются. Простые JWT не платят никогда и могут ошибаться часами. Refresh-токены платят время от времени и ошибаются около пятнадцати минут.

Алгоритм подписи: вопрос доверия

Короткая версия: HMAC — симметричный, RSA и ECDSA — асимметричные. Но это скрывает суть. Настоящий вопрос: сколько сервисов могут выпускать токен?

С HMAC ключ, который проверяет токен, тот же, что и подписывает. Значит, каждый сервис, которому вы дали ключ, может подделать токен для любого пользователя с любой ролью, и все остальные примут его за настоящий. Внутри одного монолита — норм. Между командами или рядом с третьими лицами — это слишком много доверия ради проверки подписи.

С RSA или ECDSA auth-сервис держит приватный ключ, а все остальные получают публичный. Они могут проверять сколько угодно, но не могут создать ни одного токена. Утёкший публичный ключ ничего не стоит, потому что он публичный.

Ещё одна ловушка: заголовок токена сам говорит, какой алгоритм использовать, и исторически библиотеки просто верили ему. Атакующие ставили alg=none или переключали RS256 на HS256, чтобы публичный ключ использовался как HMAC-секрет. Auth0 описала классические баги. Современные библиотеки защищаются, но всё равно закрепите алгоритм сами:

jwt.verify(token, key, { algorithms: ["RS256"] }); // никогда не давайте токену выбирать

Где хранить токен — решает, как его украдут

localStorage удобен, но доступен любому JavaScript на вашей странице. Одна плохая npm-зависимость или XSS-дыра — и токен ушёл. Никакой браузерный механизм это не остановит. HttpOnly cookie вообще нельзя прочитать из JavaScript, что убивает целый класс краж. Но браузеры автоматически отправляют cookie, чем пользуются CSRF-атаки, поэтому нужен SameSite=Lax или Strict и токен на изменяющих состояние запросах.

Заметили? Если вы положили JWT в HttpOnly cookie и проверяете серверный список отзыва — вы вернулись к сессиям, только с лишними шагами и большей кукой. Это не аргумент против JWT, это аргумент за то, чтобы понимать, какое свойство вам на самом деле нужно. Рекомендую почитать OWASP session management cheat sheet — это стоит двадцати минут.

Так что же выбрать?

Начните с ограничений, а не с аббревиатуры. Короткая версия:

  • Обычное веб-приложение с одним бэкендом? Сессии. Они проще, мгновенно отзываются, и ваш фреймворк уже умеет их делать. Redis вы не перерастёте.
  • Деньги, медицинские данные или что-то, где «разлогинен сейчас» значит сейчас? Сессии или JWT со списком отзыва — это сессии в шляпе.
  • Много сервисов или третьих лиц проверяют личность без вызова вашего auth-сервиса? JWT. Это их суперсила.

Если выбрали JWT: короткие access-токены, отзываемые refresh-токены, асимметричные ключи, закреплённый алгоритм, HttpOnly cookie. Статья Sven Slootweg «Stop using JWT for sessions» — полезный противовес хайпу, даже если вы с чем-то не согласны.

Практический вывод

Прямо сейчас посмотрите на свой код авторизации. Если вы используете JWT и уже добавили список отзыва или хранилище refresh-токенов — вы изобрели сессию. Подумайте, не проще ли использовать сессии напрямую. Если вам нужны свойства токена — используйте токен. Но не берите токен и не тратьте шесть месяцев на то, чтобы вернуть ему свойства сессии. Внимание команды ограничено, а поток ИИ-кода только усложняет поддержку безопасности.

Я строю LiveReview — AI-ревью кода с учётом радиуса взрыва для критически важных систем. Вместо того чтобы давать каждому диффу равное внимание, LiveReview оценивает каждый изменение по радиусу влияния через граф вызовов — так вы фокусируетесь на том, что реально важно. Попробуйте на своём коде.

#сессии#JWT#авторизация#безопасность#аутентификация
Al
Редакция Algolit

Пишем про алгоритмы, подготовку к собеседованиям и карьеру в IT — так, чтобы было понятно и полезно.

Хочешь закрепить знания на практике?

Решай задачи на Algolit — интерактивная платформа для обучения

Начать бесплатно →