JWT-авторизация в Express: токены на практике

Содержание

О проверке кода. Примеры не запускались в этой сессии (по договорённости — фокус на контенте, а не прогонах). Говорим прямо, а не ставим бейдж «проверено».

Авторизация — «кто ты и что тебе можно» — есть почти в каждом приложении. JWT-токены стали популярным способом её реализовать в Express. Разберём механизм и, что важнее, его безопасность.

Что такое JWT

JWT (JSON Web Token) — подписанный токен, который сервер выдаёт после успешного входа. Дальше клиент присылает его с каждым запросом, и сервер по подписи убеждается, что токен настоящий, — не спрашивая пароль снова.

Токен состоит из трёх частей через точку:

eyJhbGciOi... . eyJ1c2VySWQiOjQyfQ . SflKxwRJSM...
   заголовок        данные (payload)      подпись
Три части JWT

Заголовок + данные

Просто закодированы, НЕ зашифрованы

  • Заголовок: алгоритм подписи
  • Данные: userId, роль, срок
  • Кодировка base64 — читается любым!
  • НЕ кладите сюда секреты

Подпись

Защищает от подделки

  • Хеш от заголовка+данных с секретом
  • Проверяется секретом на сервере
  • Подделать без секрета нельзя
  • Гарантирует подлинность, не секретность

Критично понимать: данные в JWT НЕ зашифрованы, а лишь подписаны. Любой, у кого есть токен, прочитает его содержимое (это просто base64). Подпись гарантирует, что токен не подделан, но не что его не прочитают. Отсюда правило: в токен не кладут пароли и чувствительные данные — только идентификатор и роль.

Вход и выдача токена

const jwt = require('jsonwebtoken');

app.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const user = await db.users.findByEmail(email);

  // проверяем пароль (хеш сравнивают через bcrypt — см. crypto)
  if (!user || !await verifyPassword(password, user.passwordHash)) {
    return res.status(401).json({ error: 'Неверные данные' });
  }

  // выдаём токен
  const token = jwt.sign(
    { userId: user.id, role: user.role },   // данные (payload)
    process.env.JWT_SECRET,                 // секрет — из окружения!
    { expiresIn: '1h' },                    // срок истечения — обязательно
  );
  res.json({ token });
});

После проверки пароля сервер jwt.sign создаёт подписанный токен с данными пользователя, секретом и сроком. Секрет берут из переменных окружения, никогда не из кода. Пароли в БД хранят хешированными (про хеширование — в статье о crypto), а не в открытом виде — сравнивают хеши.

Middleware проверки токена

function authenticate(req, res, next) {
  const header = req.headers.authorization;   // 'Bearer <token>'
  const token = header && header.split(' ')[1];

  if (!token) {
    return res.status(401).json({ error: 'Нужна авторизация' });
  }

  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET);  // проверка подписи
    req.user = payload;   // кладём пользователя в req для обработчиков
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Неверный или истёкший токен' });
  }
}

// защищаем маршрут
app.get('/profile', authenticate, (req, res) => {
  res.json({ userId: req.user.userId });   // req.user из middleware
});

Проверка токена — классическое middleware: читает токен из заголовка Authorization, проверяет подпись через jwt.verify и кладёт данные пользователя в req.user. Защищённые маршруты подключают это middleware перед обработчиком — и внутри уже есть req.user. verify бросает на неверной или истёкшей подписи — это ловят и отвечают 401.

JWT против сессий

Стоит понимать альтернативу:

  • сессии — состояние на сервере, клиент держит лишь ID сессии. Сервер помнит, кто вошёл; легко отозвать доступ, но нужно хранилище сессий;
  • JWT — самодостаточен, сервер ничего не хранит, только проверяет подпись. Проще масштабировать (любой воркер cluster проверит токен), но отозвать токен до истечения срока сложно — сервер его не отслеживает.

Отсюда практика: JWT делают короткоживущими (expiresIn: '15m') плюс отдельный refresh-токен для обновления. Это ограничивает ущерб при утечке токена.

Где хранить токен на клиенте

Больной вопрос безопасности:

  • httpOnly-cookie — недоступна JavaScript, поэтому защищена от кражи при XSS. Безопаснее, но требует защиты от CSRF;
  • localStorage — удобнее (легко читать и слать в заголовке), но уязвим: любой вредоносный скрипт на странице прочитает токен. От этого предостерегает и статья про localStorage.

Выбор зависит от модели угроз, но для чувствительных приложений безопаснее httpOnly-cookie. Класть токены авторизации в localStorage — распространённая, но рискованная практика.

Частые ошибки безопасности

  • секрет подписи в кодетолько в переменных окружения, иначе утечёт с репозиторием, и любой подделает токены;
  • токен без срока (expiresIn) — украденный токен действует вечно;
  • чувствительные данные в payload — JWT не зашифрован, пароль или карта внутри = утечка;
  • токен в localStorage для чувствительных приложений — уязвим к XSS;
  • отсутствие проверки прав — токен подтверждает «кто ты» (аутентификация), но «что тебе можно» (авторизацию) проверяют отдельно: наличие токена ≠ право удалять чужие данные.

Смежные темы: middleware — проверка токена; crypto и хеширование паролей; localStorage и XSS — где хранить; переменные окружения — секрет подписи; валидация. Полный список — в уроках Node.js.

Частые вопросы

Что такое JWT?
JSON Web Token — подписанный токен, который сервер выдаёт после входа. Он содержит данные о пользователе и подпись, по которой сервер проверяет подлинность. Клиент присылает токен с каждым запросом вместо повторного ввода пароля.
Чем JWT отличается от сессий?
Сессия хранится на сервере, а клиент держит лишь идентификатор. JWT самодостаточен — все данные внутри токена, сервер ничего не хранит и проверяет подпись. Это упрощает масштабирование, но усложняет отзыв токена до истечения срока.
Где хранить JWT на клиенте?
Безопаснее всего в httpOnly-cookie, недоступной JavaScript, что защищает от кражи при XSS. Хранение в localStorage удобнее, но уязвимо: вредоносный скрипт прочитает токен. Выбор зависит от модели угроз приложения.
Как проверять токен в Express?
Написать middleware, которое читает токен из заголовка или cookie, проверяет подпись через библиотеку jsonwebtoken и, если всё верно, кладёт данные пользователя в req. Защищённые маршруты подключают это middleware перед обработчиком.
Какая частая ошибка при работе с JWT?
Хранить секрет подписи в коде, не ставить срок истечения токену, класть в токен чувствительные данные вроде пароля. Содержимое JWT не зашифровано, а лишь подписано, поэтому его может прочитать любой, у кого есть токен.