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

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

В JavaScript для работы с JWT одной из наиболее распространённых библиотек является Jsrsasign. Она предоставляет инструменты для создания, подписи, проверки и декодирования JWT, а также поддержку различных криптографических алгоритмов (HS256, RS256, ES256 и др.).


Библиотека позволяет:

  • создавать JWT с произвольными payload-данными
  • подписывать токены симметричными и асимметричными ключами
  • проверять подпись JWT
  • извлекать payload без проверки (для отладки)
  • работать с PEM-ключами RSA/ECDSA

Установка и подключение

В Node.js среде:

npm install jsrsasign

Использование:

const jsrsasign = require("jsrsasign");

В браузере:

<script src="https://cdnjs.cloudflare.com/ajax/libs/jsrsasign/10.8.6/jsrsasign-all-min.js"></script>

Структура JWT

JWT состоит из трёх частей:

  • Header (заголовок)
  • Payload (данные)
  • Signature (подпись)

Формат:

xxxxx.yyyyy.zzzzz

Jsrsasign автоматически формирует все три части при создании токена.


Создание JWT с HMAC (HS256)

HS256 использует общий секретный ключ для подписи и проверки.

Формирование токена

const header = { alg: "HS256", typ: "JWT" };

const payload = {
  sub: "user123",
  role: "admin",
  iat: Math.floor(Date.now() / 1000),
  exp: Math.floor(Date.now() / 1000) + 3600
};

const secret = "super_secret_key";

const token = jsrsasign.KJUR.jws.JWS.sign(
  "HS256",
  JSON.stringify(header),
  JSON.stringify(payload),
  secret
);

console.log(token);

Проверка JWT (HS256)

const isValid = jsrsasign.KJUR.jws.JWS.verifyJWT(token, secret, {
  alg: ["HS256"]
});

console.log(isValid);

Извлечение payload без проверки

const parsed = jsrsasign.KJUR.jws.JWS.parse(token);

console.log(parsed.payloadObj);

Этот способ полезен только для отладки, поскольку не гарантирует подлинность данных.


Аутентификация на основе JWT (практический сценарий)

Генерация токена при логине

function generateToken(user) {
  const header = { alg: "HS256", typ: "JWT" };

  const payload = {
    sub: user.id,
    email: user.email,
    role: user.role,
    iat: Math.floor(Date.now() / 1000),
    exp: Math.floor(Date.now() / 1000) + 7200
  };

  return jsrsasign.KJUR.jws.JWS.sign(
    "HS256",
    JSON.stringify(header),
    JSON.stringify(payload),
    process.env.JWT_SECRET
  );
}

Проверка токена в middleware (Node.js)

function authMiddleware(req, res, next) {
  const authHeader = req.headers.authorization;

  if (!authHeader) {
    res.status(401).send("No token provided");
    return;
  }

  const token = authHeader.split(" ")[1];

  const isValid = jsrsasign.KJUR.jws.JWS.verifyJWT(
    token,
    process.env.JWT_SECRET,
    { alg: ["HS256"] }
  );

  if (!isValid) {
    res.status(401).send("Invalid token");
    return;
  }

  const decoded = jsrsasign.KJUR.jws.JWS.parse(token);
  req.user = decoded.payloadObj;

  next();
}

Использование RSA (RS256)

RS256 использует пару ключей: приватный (подпись) и публичный (проверка). Это более безопасный вариант для распределённых систем.

Генерация JWT с приватным ключом

const privateKey = `-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----`;

const header = { alg: "RS256", typ: "JWT" };

const payload = {
  sub: "user123",
  role: "editor",
  exp: Math.floor(Date.now() / 1000) + 3600
};

const token = jsrsasign.KJUR.jws.JWS.sign(
  "RS256",
  JSON.stringify(header),
  JSON.stringify(payload),
  privateKey
);

Проверка JWT с публичным ключом

const publicKey = `-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----`;

const isValid = jsrsasign.KJUR.jws.JWS.verifyJWT(
  token,
  publicKey,
  { alg: ["RS256"] }
);

Работа с временем жизни токена

Ключевые поля:

  • iat — время выпуска
  • exp — срок истечения
  • nbf — не активен до указанного времени

Пример проверки срока действия:

const decoded = jsrsasign.KJUR.jws.JWS.parse(token);
const exp = decoded.payloadObj.exp;

const now = Math.floor(Date.now() / 1000);

if (now > exp) {
  console.log("Token expired");
}

Типовая архитектура JWT-аутентификации

1. Авторизация пользователя

  • проверка логина/пароля
  • генерация JWT
  • возврат клиенту

2. Хранение токена

Обычно:

  • localStorage (браузер)
  • httpOnly cookie (более безопасный вариант)

3. Отправка запроса

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

4. Проверка на сервере

  • валидация подписи
  • проверка срока действия
  • извлечение payload

Ошибки и обработка

Неверная подпись

try {
  jsrsasign.KJUR.jws.JWS.verifyJWT(token, secret, { alg: ["HS256"] });
} catch (e) {
  console.log("Invalid signature");
}

Повреждённый токен

try {
  jsrsasign.KJUR.jws.JWS.parse(token);
} catch (e) {
  console.log("Malformed token");
}

Безопасность при использовании JWT

Ключевые аспекты:

  • секрет HS256 должен быть длинным и случайным
  • RS256 предпочтительнее для распределённых систем
  • срок жизни токена должен быть ограничен
  • не хранить чувствительные данные в payload
  • всегда проверять exp

Расширенный пример: роли и доступ

function authorizeRole(role) {
  return function (req, res, next) {
    const token = req.headers.authorization?.split(" ")[1];

    const decoded = jsrsasign.KJUR.jws.JWS.parse(token);

    if (decoded.payloadObj.role !== role) {
      res.status(403).send("Forbidden");
      return;
    }

    next();
  };
}

Обновление токенов (refresh tokens)

JWT обычно не обновляется напрямую. Используется пара:

  • access token (короткий срок)
  • refresh token (долгий срок)

Логика:

  1. access token истёк
  2. отправка refresh token
  3. выдача нового access token

Пример генерации refresh token

const refreshToken = jsrsasign.KJUR.jws.JWS.sign(
  "HS256",
  JSON.stringify({ alg: "HS256", typ: "JWT" }),
  JSON.stringify({
    sub: user.id,
    type: "refresh",
    exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30
  }),
  secret
);

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

const decoded = jsrsasign.KJUR.jws.JWS.parse(token);

if (decoded.payloadObj.type !== "refresh") {
  throw new Error("Invalid token type");
}

Интеграция с фронтендом

async function login() {
  const res = await fetch("/login", {
    method: "POST",
    body: JSON.stringify({ email, password })
  });

  const data = await res.json();

  localStorage.setItem("token", data.token);
}

Использование токена на клиенте

async function getProfile() {
  const token = localStorage.getItem("token");

  const res = await fetch("/profile", {
    headers: {
      Authorization: "Bearer " + token
    }
  });

  return await res.json();
}