Головна / Статті / Сесії проти 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 все ще залишаються корисними.