JWT (JSON Web Token) это строка из трёх частей в base64url, header.payload.signature, которую сервер выдаёт пользователю при входе и проверяет на каждом следующем запросе. Подпись доказывает, что токен выдал сервер и никто его не менял, но payload только закодирован, а не зашифрован, поэтому прочитать его может любой. В Node.js токен создают через new SignJWT(payload).sign(secret) из пакета jose и проверяют через jwtVerify(token, secret).
Программа выводит 3 parts, затем user-42 admin: jwtVerify проверил подпись и срок действия и вернул payload. Измените на один символ секрет, который передаётся в jwtVerify, и запустите снова: проверка упадёт с signature verification failed.
Что внутри JWT
Header и payload это JSON, закодированный в base64url. Для декодирования ключ не нужен, поэтому в JWT нельзя класть пароль или любые другие секреты.
Первые две строки выводят {"alg":"HS256"} и {"name":"Ada","role":"editor"}, их может прочитать любой, у кого есть токен. Изменить их мешает подпись (43 символа для HS256): поменяйте editor на admin, и подпись перестанет совпадать.
| Часть | Что содержит | Секретна? |
|---|---|---|
| Header | Алгоритм, например HS256 | Нет, читается |
| Payload | Claims: sub (id пользователя), exp (срок действия), iat (время выдачи), ваши поля | Нет, читается |
| Signature | HMAC или RSA подпись первых двух частей | Для создания нужен ключ |
Срок действия и неудачная проверка
setExpirationTime('15m') записывает claim exp, и jwtVerify отклоняет токен, срок которого истёк. Каждый отказ это ошибка с полем code, так что просроченный токен можно отличить от поддельного.
Первая проверка падает с ERR_JWT_EXPIRED, а вторая, где подпись изменена, с ERR_JWS_SIGNATURE_VERIFICATION_FAILED. Для вашего API оба случая значат одно и то же (ответ 401), но код в логах помогает при отладке.
Защитить маршрут Express Bearer токеном
Клиент один раз входит, сохраняет токен и отправляет его в заголовке Authorization в виде Bearer <token>. Middleware проверяет токен и кладёт payload в req.user для маршрута.
Первый запрос, GET /me без токена, получает 401 и Missing token. Отправьте POST /login с телом {"username": "ada", "password": "lovelace"}, скопируйте token из ответа и снова отправьте GET /me с заголовком Authorization: Bearer <token>: ответ будет {"id":"user-1","name":"ada"}.
Пакет jsonwebtoken
jsonwebtoken (npm install jsonwebtoken) это другой популярный вариант. Его sign и verify синхронные (с необязательными callback), а не возвращают промисы:
const jwt = require('jsonwebtoken');
const token = jwt.sign({ role: 'admin' }, process.env.JWT_SECRET, {
subject: 'user-42',
expiresIn: '1h',
});
const payload = jwt.verify(token, process.env.JWT_SECRET); // throws if invalid
Для нового кода берите jose: у него нет зависимостей, и он работает также в edge runtime и в браузерах. Обе библиотеки выдают стандартные JWT, поэтому одна может проверить то, что подписала другая.
Частые ошибки
decode вместо verify. jwt.decode и decodeJwt читают payload без проверки подписи, так что поддельный токен пройдёт. Доверяйте только тому, что вернул verify.
Короткий или закоммиченный секрет. Угадываемый секрет HS256 позволяет атакующему подписывать свои токены. Используйте минимум 32 случайных байта, читайте секрет из process.env и никогда не коммитьте его.
Токены без срока действия. Без exp утёкший токен работает вечно. Всегда задавайте срок действия.
Часто задаваемые вопросы
Где клиенту хранить JWT?
В браузерном приложении безопаснее всего cookie с флагами HttpOnly и Secure, потому что скрипты страницы не могут её прочитать. localStorage проще, но любой скрипт на странице, в том числе внедрённый, прочитает токен. Мобильные приложения используют защищённое хранилище платформы.
Как разлогинить пользователя при JWT?
Сервер не может отозвать уже подписанный токен: он действует до истечения срока. Удалите его на клиенте, делайте access токены короткоживущими, а если нужен мгновенный выход, храните на сервере список отозванных id токенов (jti) и проверяйте его на каждом запросе.
HS256 или RS256?
HS256 использует один общий секрет и для подписи, и для проверки, это подходит для одного бэкенда. RS256 подписывает приватным ключом, а проверяет публичным, так что другие сервисы могут проверять токены, не имея возможности их создавать.
Сколько должен действовать JWT?
Access токены обычно действуют от 5 до 60 минут. Для длинных сессий добавьте refresh токен, который хранится аккуратнее и который клиент обменивает на новый access токен.