Необходимый минимум для понимания JWT. Что, зачем и почему

Frontend

Уверен, вы уже знаете про авторизацию через куки. Они идут в комплекте с такими понятиями как сессии, CSRF и CORS. Но в современных приложениях мы всё чаще используем JWT — JSON Web Token. Сегодня разберем, что это такое, как оно устроено, зачем нужно, в чем его достоинства и недостатки.

Что такое JWT простыми словами

JWT (JSON Web Token) — это компактный способ передачи данных между двумя сторонами в виде JSON-объекта. Данные подписаны цифровой подписью, поэтому их можно проверить и доверять им.

JWT состоит из трех частей (в записи токена они разделены точками).

  1. Header (заголовок) — информация о типе токена и алгоритме подписи.
  2. Payload (полезная нагрузка) — данные (кто пользователь, его роль, время истечения).
  3. Signature (подпись) — криптографическая подпись, которая защищает от подделки.

Все части кодируются (не шифруются!) в Base64Url (читаемый текст, но безопасный для передачи).

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Составные части JWT

Общий вид токена выше. Ниже его Header и Payload представлены в декодированном из Base64Url виде.

1. Header

{
    "alg": "HS256", // алгоритм подписи (HMAC SHA256)
    "typ": "JWT"    // тип токена
}

2. Payload

В токене можно закодировать практически любую полезную информацию, например:

{
    "id": "1234567890",   // идентификатор пользователя
    "name": "John Doe",    // имя пользователя
    "iat": 1516239022,     // время выпуска (issued at)
    "exp": 1516242622      // время истечения (expiration)
}

3. Signature

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

HMACSHA256(
    base64UrlEncode(header) + "." + base64UrlEncode(payload),
    secret
)

Изначально секретный ключ известен только сервису аутентификации. Он сообщает его серверу, чтобы тот мог расшифровать подпись и убедиться в подлинности токена.

Как работает JWT: пошагово

Шаг 1. Пользователь входит в систему

Пользователь → Сервер: POST /login с логином и паролем

Шаг 2. Сервер проверяет данные и создает JWT

Сервер проверяет логин/пароль, затем:

  • Создает payload с данными пользователя.
  • Добавляет время истечения (например, через 1 час).
  • Подписывает header + payload своим секретным ключом.

Здесь и далее приведены примеры кода на Node.js

const jwt = require('jsonwebtoken');
const payload = {
    userId: user.id,
    email: user.email,
    role: user.role
};
const token = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '1h' });

Шаг 3. Сервер отправляет токен клиенту

Сервер → Клиент: { "token": "eyJhbGci..." }

Клиент (фронтенд) сохраняет токен:

  • В localStorage / sessionStorage (рискуя при этом стать жертвой XSS)
  • В HttpOnly cookie (безопаснее, но необходимо позаботиться о защите от CSRF)

Шаг 4. Клиент отправляет токен с каждым запросом

fetch('/api/profile', {
    headers: {
        'Authorization': 'Bearer ' + token
    }
});

Шаг 5. Сервер проверяет токен при каждом запросе

const authHeader = req.headers.authorization;
const token = authHeader && authHeader.split(' ')[1];

if (!token) {
    return res.status(401).json({ error: 'Нет токена' });
}

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

Преимущества JWT

1. Не нужно хранить сессии на сервере

Сервер не хранит информацию о токенах. Он просто проверяет подпись. Это позволяет легко масштабироваться — не нужна общая база сессий для разных инстансов приложения.

2. Самодостаточность

Токен может содержать все необходимые на данные о пользователе. Не нужно ходить в базу за каждым запросом, чтобы узнать, кто это.

3. Кросс-доменная аутентификация

JWT можно использовать на нескольких разных доменах сразу (в отличие от кук с SameSite). Это то что просто необходимо для мобильных приложений, микросервисов и Single Page Application.

4. Легко передавать между сервисами

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

Недостатки JWT

1. Нельзя отозвать токен

Токен перестает быть действительным только после истечения срока его действия. То есть, если вы выдали токен на 1 час, а пользователь за это время сменил пароль или вышел из системы — токен все равно будет валидным.

Но это решаемо. Можно:

  • Делать короткое время жизни (15-30 минут) и использовать refresh token.
  • Хранить черный список (blocklist) отозванных токенов (но частично теряется преимущество номер 1 — приходится хранить что-то на сервере).

2. Размер токена

JWT может быть большим, если в payload много данных. Каждый запрос несет этот токен — трафик растет.

3. Секретный ключ нужно хранить в безопасности

Если злоумышленник украдет JWT_SECRET, он сможет подписывать любые токены. В любом количестве. Это все равно что украсть печать компании.

4. XSS-уязвимость (если хранить в localStorage)

Если у злоумышленника получится внедрить скрипт на страницу, он сможет прочитать localStorage и украсть токен.

Как мы уже говорили выше, безопаснее хранить токен в HttpOnly cookie. При этом у куки должен быть параметры SameSite=Lax/Strict и плюсом к этому — добавить CSRF-токен для защиты.

Access Token vs Refresh Token

Если токен долго живет, его сложно отозвать. Если срок жизни невелик — пользователя постоянно разлогинивает.

Решение: два токена.

  • Access Token — короткоживущий (5-30 минут). Используется для доступа к API.
  • Refresh Token — долгоживущий (дни/месяцы). Используется только для получения нового Access Token.
// При входе выдается два токена
const accessToken = jwt.sign(payload, JWT_SECRET, { expiresIn: '15m' });
const refreshToken = jwt.sign(payload, JWT_SECRET_REFRESH, { expiresIn: '7d' });

Как и раньше, клиент использует accessToken для запросов к API. Но он быстро истекает и после этого, клиент отправляет refreshToken на отдельный endpoint. В ответ он получает новый accessToken.

Сравним с сессиями

КритерийJWTСессии (Session)
Хранение данныхНа клиентеНа сервере (БД / Redis)
StatelessДаНет
МасштабированиеЛегко (любой сервер)Нужна общая БД сессий
Отзыв токена/сессииСложно (до истечения)Легко (удалить сессию)
Размер запросаБольше (токен в каждом запросе)Меньше (только cookie с ID сессии)
УязвимостьXSS (в localStorage)CSRF (в куках)

Что нельзя хранить в JWT

Никогда не храните в payload чувствительные данные:

{
    "userId": 123,
    "password": "12345",        // НИКОГДА
    "bankCard": "4111-1111",  // НИКОГДА
    "passport": "123-45-6789"        // НИКОГДА
}

Payload только кодируется в Base64, но не шифруется. Любой может декодировать и прочитать данные.

Что можно хранить:

  • userId (идентификатор, не секретный)
  • email (публичная информация)
  • role (права доступа)
  • iatexp (технические метки)

Итоги

  • JWT — это JSON-объект с цифровой подписью. Он самодостаточен и не требует хранения на сервере.
  • Состоит из Header, Payload и Signature.
  • Payload только кодируется, а не шифруется.
  • Подпись проверяется с помощью секретного ключа. Без него токен нельзя подделать.

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

Желаю успехов!

Симо Мофин
Симо Мофин

Senior Frontend Developer
Главный по блогу