Главная / Статьи / Сессии против JWT: как выбрать подходящую модель аутентификации в Node.js

Сессии против JWT: как выбрать подходящую модель аутентификации в Node.js

Узнайте, в чём на самом деле заключаются различия между сессиями и JWT в процессе аутентификации в Node.js, как устроены каждый из этих методов, и как выбрать между ними, чтобы позже не пожалеть.

1335 слов

Проблема, скрытая за обоими подходами

У HTTP нет встроенной памяти, сохраняющей информацию между запросами. Каждый входящий запрос кажется исходящим от совершенно незнакомого пользователя, если только он не содержит доказательств его идентичности. И сессии, и JWT решают эту проблему одинаковым основным способом: они предоставляют клиенту данные, которые тот должен вернуть при каждом последующем запросе. Различие между ними заключается в характере этих данных, а ещё более важно — в том, где фактически хранится авторитетная информация о том, «кто вошёл в систему».

Вариант один: сессии

В схеме, основанной на сессиях, авторитет находится у сервера. После входа пользователя сервер генерирует случайный идентификатор сессии, сохраняет реальную информацию пользователя при этом идентификаторе в таких системах, как Redis или база данных, и передаёт клиенту только сам идентификатор, обычно хранящийся в куки.

app.post("/login", async (req, res) => {
  const user = await authenticate(req.body.email, req.body.password);
  const sessionId = generateSecureId();

  await redis.set(`session:${sessionId}`, JSON.stringify({ userId: user.id }), "EX", 86400);
  res.cookie("sessionId", sessionId, { httpOnly: true, secure: true });
  res.json({ success: true });
});

app.use(async (req, res, next) => {
  const sessionId = req.cookies.sessionId;
  const session = await redis.get(`session:${sessionId}`);
  req.user = session ? JSON.parse(session).userId : null;
  next();
});

С этого момента каждый входящий запрос запускает поиск в том месте, где хранятся данные сессии. Именно этот механизм объясняет как пользу сессий, так и причину возникновения дополнительной нагрузки.

Плюсы: вы получаете немедленный и полный контроль над теми, кто находится вошедшим в систему. Завершение сессии, будь то выход пользователя, сброс пароля или отключение администратором скомпрометированной учетной записи, сводится к простому удалению записи в хранилище сессий. Нет необходимости ждать, пока что-то истечет по времени.

Минусы: теперь каждый запрос требует двухэтапной обработки в хранилище сессий, что добавляет небольшую, но реальную задержку и нагрузку на инфраструктуру. Кроме того, хранилище сессий превращается в общедоступное состояние, к которому должны иметь доступ все серверы, что становится серьезной архитектурной проблемой при масштабировании за пределы одного сервера.

Вариант два: JWT

JWT (JSON Web Token) работает иначе: сервер подписывает токен, в котором уже содержатся данные пользователя, а клиент пересылает именно этот токен с каждым запросом. Сервер проверяет подпись и доверяет содержимому токена, не нуждаясь в поиске информации в других источниках.

app.post("/login", async (req, res) => {
  const user = await authenticate(req.body.email, req.body.password);
  const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { expiresIn: "1h" });
  res.cookie("token", token, { httpOnly: true, secure: true });
  res.json({ success: true });
});


app.use((req, res, next) => {
  try {
    const decoded = jwt.verify(req.cookies.token, process.env.JWT_SECRET);
    req.user = decoded.userId;
  } catch {
    req.user = null;
  }
  next();
});

Не требуется ни обращение к базе данных, ни обращение к Redis. Сама криптографическая подпись достаточна для подтверждения того, что с момента выдачи токен не был изменён.

Преимущества: отсутствуют поиски в базе данных или кэше для каждого запроса, что значительно ускоряет работу, а также нет общего хранилища сессий, к которому должны обращаться все серверы, что упрощает горизонтальное масштабирование и делает архитектуры безсостоянийных микросервисов гораздо более понятными для анализа.

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

Заблуждение, вызывающее реальные проблемы

Фраза «JWTs являются безсостоянийными» чрезмерно часто используется в качестве безоговорочного преимущества, из-за чего люди упускают из виду её истинный смысл: безсостоянийность по умолчанию также означает отсутствие возможности аннулирования. Если куки сессии компрометированы, их достаточно просто удалить. В то же время украденный JWT остаётся полностью действительным и пользуется полной доверием сервера в течение всего срока его действия, если только вы специально не разработаете что-то дополнительное для предотвращения этого.

Типичным решением, к которому прибегают люди, является ведение списка заблокированных токенов:

app.use(async (req, res, next) => {
  try {
    const decoded = jwt.verify(req.cookies.token, process.env.JWT_SECRET);
    const isRevoked = await redis.get(`revoked:${decoded.jti}`);
    if (isRevoked) throw new Error("Token revoked");
    req.user = decoded.userId;
  } catch {
    req.user = null;
  }
  next();
});

Этот подход действительно работает, но внимательно посмотрите на то, что вы только что сделали: вы вновь ввели проверку для каждого запроса против общего хранилища данных, что именно является лишней нагрузкой, от которой должны были избавить JWT. Теперь у вас уже нет по-настоящему бессостоянийной системы. У вас есть система, основанная на сессиях, скрывающаяся под другим видом, с более запутанной структурой обработки сбоев.

Так какую же систему следует использовать?

Используйте сессии, когда: вам нужна немедленная и надежная отзыв возможности доступа (например, в банковских приложениях, панелях управления администраторами или в любых системах с высокими требованиями к безопасности), вы запускаете одно приложение за балансировщиком нагрузки, который может без проблем обращаться к общему хранилищу сессий, или вам проще поддерживать единственный источник правды, чем разбираться с сроками действия токенов и логикой блокировки.

Используйте JWT в следующих случаях: когда вы обрабатываете аутентификацию в полностью независимых сервисах, которые не должны все иметь прямой доступ к одному общему хранилищу сессий, когда вы создаете систему, в которой короткий срок действия токенов (минуты вместо дней) позволяет считать интервал отзыва приемлемым, или когда настоящая причина использования безсостоянийного подхода заключается в обслуживании множества независимых потребителей API, а не просто в том, что это звучит более логично.

Один нюанс, на который стоит обратить внимание: на практике большинство производственных систем не выбирают один из вариантов окончательно. Универсальным решением являются краткосрочные JWT в сочетании с токеном обновления, хранящимся на стороне сервера. Это скорее смешанное решение, чем дихотомия: сессии обеспечивают долгосрочное управление доверием и отзывом прав, в то время как JWT используются для краткосрочной, безсостоянийной верификации между ними. Если вы рассматриваете вопрос «сессии против JWT» как выбор между двумя альтернативами, это обычно означает, что вы ещё не достигли такого уровня масштабирования, при котором такая гибридная конфигурация оправдывает свою сложность; в таком случае простая сессия, скорее всего, будет более честным и простым отправной пунктом.

Финальное решение

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

Связанные материалы

  • Выбор между EC2, ECS и EKS для задач Node.js — Сравнение способов работы EC2, ECS с Fargate и EKS с приложениями Node.js с точки зрения операционных аспектов, которое поможет вам выбрать подходящий сервис обработки вычислений AWS в зависимости от масштабов и навыков вашей команды.
  • Реализация токенов доступа и обновления вместе в Node.js — Узнайте, как объединить краткосрочные токены доступа с циклически обновляемыми токенами обновления в Node.js для обеспечения баланса между безопасностью и бесперебойными сессиями пользователей.
  • Создание системы аутентификации JWT, готовой к использованию в производстве, для API на Node.js — Как хэшировать пароли, выдавать краткосрочные токены JWT, добавлять токены обновления и механизмы отзыва, разделять процессы авторизации и аутентификации, защищаться от атак типа brute force и проверять корректную работу механизмов отказа.
  • Когда аутентификация JWT становится зависимой от состояния: аргументы в пользу использования сессий на стороне сервера в Node — Рассмотрим, как списки блокировки и хранилища токенов обновления делают систему аутентификации JWT уязвимой, как сессии в Express с поддержкой Postgres упрощают её работу и в каких случаях JWT всё ещё остаются подходящим решением.