Аутентификация через JWT

JWT — самодостаточный токен: сервер выдаёт его при входе, клиент предъявляет в каждом запросе.
«JWT — это пропуск с печатью. Подделать содержимое нельзя — печать (подпись) не сойдётся.»

HTTP не помнит, кто ты: каждый запрос независим. Чтобы сервер узнавал авторизованного пользователя, после входа ему выдают токен, который он прикладывает к последующим запросам. Самый распространённый формат — JWT (JSON Web Token). В этом уроке разберём его устройство и напишем middleware-защиту.

Из чего состоит JWT

JWT — это строка из трёх частей, разделённых точками: заголовок, полезная нагрузка и подпись. Первые две — это закодированный в base64 JSON, третья — криптографическая подпись:

header.payload.signature

eyJhbGciOiJIUzI1NiJ9 . eyJpZCI6MSwicm9sZSI6InVzZXIifQ . SflKxw...
     |                        |                            |
  {alg:HS256}          {id:1, role:user}            подпись секретом
  (base64)               (base64)                   (нельзя подделать)

Важно понять: payload не зашифрован, а лишь закодирован — любой может его прочитать. Секрет защищает не от чтения, а от подделки: без него нельзя сделать подпись для изменённого payload.

Поток аутентификации

1. POST /login (email+пароль)
        |
        v
   сервер проверил пароль -> выдал JWT
        |
        v
2. GET /profile
   Authorization: Bearer <JWT>
        |
        v
   middleware проверил подпись -> req.user = {...} -> доступ

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

const jwt = require('jsonwebtoken');
const SECRET = process.env.JWT_SECRET;

// при входе -- выдаём токен на 15 минут
const token = jwt.sign({ id: user.id, role: user.role }, SECRET, {
  expiresIn: '15m'
});

// middleware-защита: проверяет токен из заголовка
function auth(req, res, next) {
  const header = req.get('Authorization') || '';
  const token = header.replace('Bearer ', '');
  try {
    req.user = jwt.verify(token, SECRET);  // упадёт, если подпись неверна
    next();
  } catch (e) {
    res.status(401).json({ error: 'нужна авторизация' });
  }
}

app.get('/profile', auth, (req, res) => res.json(req.user));

Декодируем payload в браузере

Разберём структуру JWT руками: расшифруем base64-части и достанем данные. Так видно, что payload открыт для чтения (но защищён от подделки):

// собираем учебный токен (без настоящей подписи)
const header = btoa(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
const payload = btoa(JSON.stringify({ id: 1, role: 'admin' }));
const token = header + '.' + payload + '.signature';

console.log('Токен:', token);

// клиент (или кто угодно) может прочитать payload:
const parts = token.split('.');
const decoded = JSON.parse(atob(parts[1]));
console.log('Внутри:', decoded); // { id: 1, role: 'admin' }
// но изменить и переподписать без секрета -- нельзя

Как работает под капотом

Подпись считается так: сервер берёт header.payload, прогоняет через алгоритм (например HS256) с секретным ключом и получает подпись. При проверке он повторяет вычисление и сравнивает результат с подписью из токена. Если кто-то изменил payload, подпись не совпадёт — токен отвергается. Секрет известен только серверу, поэтому подделать подпись нельзя.

Частые ошибки

  • Класть секреты в payload. Он читается всеми. Никаких паролей и приватных данных.
  • Вечные токены. Задавай expiresIn; для долгих сессий используй пару access+refresh.
  • Слабый или захардкоженный секрет. Минимум 32 случайных символа из переменной окружения.

Best practices

  • Короткий access-токен (15 мин) плюс долгий refresh-токен в базе для отзыва.
  • Храни секрет в process.env.JWT_SECRET, не в коде.
  • Проверяй токен в middleware и клади пользователя в req.user для маршрутов.

Итоги

JWT — самодостаточный подписанный пропуск: сервер выдаёт его при входе, клиент предъявляет в заголовке, middleware проверяет подпись. Payload открыт для чтения, но защищён от подделки секретом. Теперь у нас есть полноценная авторизация; в следующем разделе займёмся безопасностью всего приложения и его выкаткой в прод.

Access и refresh: зачем два токена

У JWT есть встроенный недостаток: его нельзя "отозвать" до истечения срока, ведь сервер не хранит выданные токены. Решение — пара токенов. Короткоживущий access-токен (15 минут) ходит в каждом запросе; если он утечёт, окно злоупотребления невелико. Долгоживущий refresh-токен (дни) хранится в базе и обменивается на новый access, когда тот истёк. Поскольку refresh лежит в базе, его можно пометить отозванным при выходе, смене пароля или подозрении на взлом — и пользователь мгновенно теряет доступ. Эта схема сочетает удобство самодостаточного JWT с возможностью немедленного отзыва, поэтому именно её используют в большинстве серьёзных приложений.

Проверьте себя
1. Что защищает подпись JWT?
AСкрывает payload от чтения
BНе даёт подделать содержимое токена без секрета
CШифрует пароль пользователя
DУскоряет проверку токена
2. Почему нельзя класть в payload JWT пароль пользователя?
APayload слишком короткий
BPayload только закодирован base64 и легко читается любым
CJWT не поддерживает строки
DЭто замедлит проверку