Реализация JWT аутентификации для веб-приложения

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.
Разработка и обслуживание любых видов сайтов:
Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация JWT аутентификации для веб-приложения
Средняя
~2-3 рабочих дня
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Реализация JWT аутентификации для веб-приложения

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

Структура JWT

Токен состоит из трёх base64url-частей, разделённых точкой:

header.payload.signature

// Декодированный header:
{ "alg": "RS256", "typ": "JWT" }

// Декодированный payload:
{
  "sub": "user_42",        // subject — ID пользователя
  "iss": "api.example.com", // issuer
  "aud": "app.example.com", // audience
  "iat": 1735600000,        // issued at
  "exp": 1735686400,        // expires at (24 часа)
  "jti": "uuid-v4",         // JWT ID для revocation
  "roles": ["admin"],
  "plan": "pro"
}

Подпись — HMAC-SHA256 (HS256) или RSA/ECDSA (RS256/ES256). RS256 предпочтительнее: приватный ключ только на сервере выдачи, публичный можно раздать любому ресурсному серверу для верификации.

Реализация (Node.js + jose)

import { SignJWT, jwtVerify, generateKeyPair } from 'jose';

// Генерация ключевой пары (один раз, храним в секретах)
const { privateKey, publicKey } = await generateKeyPair('RS256');

// Выдача токена
async function issueTokens(userId: string, roles: string[]) {
  const now = Math.floor(Date.now() / 1000);

  const accessToken = await new SignJWT({ roles, plan: 'pro' })
    .setProtectedHeader({ alg: 'RS256' })
    .setSubject(userId)
    .setIssuedAt(now)
    .setExpirationTime('15m')  // короткоживущий
    .setJti(crypto.randomUUID())
    .sign(privateKey);

  const refreshToken = await new SignJWT({})
    .setProtectedHeader({ alg: 'RS256' })
    .setSubject(userId)
    .setIssuedAt(now)
    .setExpirationTime('30d')  // долгоживущий
    .setJti(crypto.randomUUID())
    .sign(privateKey);

  return { accessToken, refreshToken };
}

// Верификация
async function verifyToken(token: string) {
  const { payload } = await jwtVerify(token, publicKey, {
    issuer: 'api.example.com',
    audience: 'app.example.com',
  });
  return payload;
}

Access Token + Refresh Token стратегия

Access token живёт 15 минут — минимальное окно при компрометации. Refresh token — 30 дней, хранится безопасно:

// При логине — устанавливаем refresh token в httpOnly cookie
res.cookie('refresh_token', refreshToken, {
  httpOnly: true,        // недоступен JS
  secure: true,          // только HTTPS
  sameSite: 'strict',    // CSRF-защита
  maxAge: 30 * 24 * 3600 * 1000,
  path: '/api/auth/refresh', // cookie отправляется ТОЛЬКО на /refresh
});

// Access token — в памяти (React state или module variable), НЕ в localStorage
// localStorage уязвим к XSS

Эндпоинт обновления:

app.post('/api/auth/refresh', async (req, res) => {
  const refreshToken = req.cookies.refresh_token;
  if (!refreshToken) return res.status(401).json({ error: 'No refresh token' });

  const payload = await verifyToken(refreshToken);

  // Проверяем, не отозван ли токен (по jti)
  const isRevoked = await redis.get(`revoked:${payload.jti}`);
  if (isRevoked) return res.status(401).json({ error: 'Token revoked' });

  const user = await db.user.findUnique({ where: { id: payload.sub } });
  const tokens = await issueTokens(user.id, user.roles);

  // Ротируем refresh token
  await redis.setex(`revoked:${payload.jti}`, 30 * 24 * 3600, '1');
  res.cookie('refresh_token', tokens.refreshToken, { /* ... */ });
  res.json({ access_token: tokens.accessToken });
});

Revocation через Redis blocklist

JWT stateless — нельзя «отозвать» без дополнительного хранилища. Решение — blocklist с TTL:

// Logout
app.post('/api/auth/logout', authenticate, async (req, res) => {
  const { jti, exp } = req.jwtPayload;
  const ttl = exp - Math.floor(Date.now() / 1000);

  if (ttl > 0) {
    await redis.setex(`revoked:${jti}`, ttl, '1');
  }

  res.clearCookie('refresh_token', { path: '/api/auth/refresh' });
  res.json({ success: true });
});

// Middleware верификации с проверкой blocklist
async function authenticate(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  const payload = await verifyToken(token);

  const isRevoked = await redis.get(`revoked:${payload.jti}`);
  if (isRevoked) return res.status(401).json({ error: 'Token revoked' });

  req.jwtPayload = payload;
  next();
}

Laravel — реализация через tymon/jwt-auth

composer require tymon/jwt-auth
php artisan jwt:secret
// config/auth.php
'guards' => [
    'api' => ['driver' => 'jwt', 'provider' => 'users'],
],

// AuthController
public function login(LoginRequest $request)
{
    $credentials = $request->only(['email', 'password']);

    if (!$token = auth('api')->attempt($credentials)) {
        return response()->json(['error' => 'Invalid credentials'], 401);
    }

    return response()->json([
        'access_token' => $token,
        'token_type'   => 'bearer',
        'expires_in'   => auth('api')->factory()->getTTL() * 60,
    ]);
}

public function refresh()
{
    return response()->json([
        'access_token' => auth('api')->refresh(),
    ]);
}

Что не класть в JWT

JWT виден всем, кто перехватит токен (до верификации подписи) — подпись гарантирует целостность, не конфиденциальность. Не класть в payload:

  • Пароли, секреты
  • Платёжные данные
  • Персональные данные сверх необходимого (GDPR — минимизация)

Оптимальный payload: sub (user_id), roles, plan, jti, стандартные claims.

Сроки

JWT auth с RS256, access+refresh токены, httpOnly cookie для refresh, Redis revocation, logout: 3–5 дней. С автообновлением токена в React (silent refresh), поддержкой нескольких устройств, аудит-логом сессий: 1–2 недели.