← Вернуться к блогу

JSON Web Tokens (JWT): Полное руководство по аутентификации

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

Big JSON Team15 мин чтенияsecurity
B

Big JSON Team

Technical Writer

Expert in JSON data manipulation, API development, and web technologies. Passionate about creating tools that make developers' lives easier.

15 мин чтения

# JSON Web Tokens (JWT): Полное руководство по аутентификации

JSON Web Tokens (JWT) — это открытый стандарт (RFC 7519) для безопасной передачи информации между сторонами в виде JSON объекта. В этом руководстве мы разберем, как работает JWT и как правильно его использовать.

Что такое JWT?

JWT — это компактный, URL-безопасный способ представления claims (утверждений) для передачи между двумя сторонами.

Основные характеристики

  • Самодостаточный: Токен содержит всю необходимую информацию о пользователе
  • Компактный: Может передаваться через URL, POST параметр или HTTP заголовок
  • Безопасный: Подписан или зашифрован

Когда использовать JWT?

  • Аутентификация: Самый распространенный сценарий
  • Обмен информацией: Безопасная передача данных между сторонами
  • Single Sign-On (SSO): Централизованная аутентификация
  • Структура JWT

    JWT состоит из трех частей, разделенных точками:

    Header.Payload.Signature

    Визуальный пример

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
    

    .

    eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6ItCY0LLQsNC9IiwiYWRtaW4iOnRydWV9

    .

    SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

    1. Header (Заголовок)

    Содержит метаданные о токене:

    {
    

    "alg": "HS256",

    "typ": "JWT"

    }

    • alg: Алгоритм подписи (HS256, RS256, и т.д.)
    • typ: Тип токена (всегда JWT)

    Затем header кодируется в Base64URL.

    2. Payload (Полезная нагрузка)

    Содержит claims — утверждения о пользователе:

    {
    

    "sub": "1234567890",

    "name": "Иван",

    "admin": true,

    "iat": 1516239022,

    "exp": 1516242622

    }

    Типы claims: Registered (Зарегистрированные):
    • iss (issuer): Издатель токена
    • sub (subject): Субъект (ID пользователя)
    • aud (audience): Аудитория
    • exp (expiration): Время истечения
    • nbf (not before): Не использовать до
    • iat (issued at): Время создания
    • jti (JWT ID): Уникальный ID токена

    Public (Публичные):

    Определяются пользователем, но должны быть зарегистрированы в IANA JWT Registry.

    Private (Частные):

    Пользовательские claims для обмена информацией.

    3. Signature (Подпись)

    Гарантирует целостность токена:

    HMACSHA256(
    

    base64UrlEncode(header) + "." +

    base64UrlEncode(payload),

    secret

    )

    Создание JWT

    Node.js с jsonwebtoken

    const jwt = require('jsonwebtoken');
    
    

    // Секретный ключ (храните в переменных окружения!)

    const СЕКРЕТ = process.env.JWT_SECRET || 'ваш-супер-секретный-ключ';

    // Создание токена

    function создатьТокен(пользователь) {

    const payload = {

    sub: пользователь.id,

    name: пользователь.имя,

    email: пользователь.email,

    роль: пользователь.роль

    };

    const опции = {

    expiresIn: '24h', // Токен истекает через 24 часа

    issuer: 'ваше-приложение',

    audience: 'пользователи-приложения'

    };

    return jwt.sign(payload, СЕКРЕТ, опции);

    }

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

    const пользователь = {

    id: 123,

    имя: 'Иван Петров',

    email: 'ivan@example.com',

    роль: 'admin'

    };

    const токен = создатьТокен(пользователь);

    console.log('JWT:', токен);

    Python с PyJWT

    import jwt
    

    import datetime

    import os

    # Секретный ключ

    СЕКРЕТ = os.getenv('JWT_SECRET', 'ваш-супер-секретный-ключ')

    def создать_токен(пользователь):

    """

    Создание JWT токена

    """

    payload = {

    'sub': пользователь['id'],

    'name': пользователь['имя'],

    'email': пользователь['email'],

    'роль': пользователь['роль'],

    'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=24),

    'iat': datetime.datetime.utcnow(),

    'iss': 'ваше-приложение'

    }

    токен = jwt.encode(payload, СЕКРЕТ, algorithm='HS256')

    return токен

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

    пользователь = {

    'id': 123,

    'имя': 'Иван Петров',

    'email': 'ivan@example.com',

    'роль': 'admin'

    }

    токен = создать_токен(пользователь)

    print(f'JWT: {токен}')

    Проверка JWT

    Node.js

    const jwt = require('jsonwebtoken');
    
    

    function проверитьТокен(токен) {

    try {

    const payload = jwt.verify(токен, СЕКРЕТ, {

    issuer: 'ваше-приложение',

    audience: 'пользователи-приложения'

    });

    console.log('Токен валиден');

    console.log('Пользователь:', payload.name);

    console.log('Email:', payload.email);

    return payload;

    } catch (err) {

    if (err.name === 'TokenExpiredError') {

    console.error('Токен истек');

    } else if (err.name === 'JsonWebTokenError') {

    console.error('Невалидный токен');

    } else {

    console.error('Ошибка проверки:', err.message);

    }

    return null;

    }

    }

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

    const токен = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';

    const payload = проверитьТокен(токен);

    if (payload) {

    console.log('Доступ разрешен');

    }

    Python

    import jwt
    
    

    def проверить_токен(токен):

    """

    Проверка JWT токена

    """

    try:

    payload = jwt.decode(

    токен,

    СЕКРЕТ,

    algorithms=['HS256'],

    issuer='ваше-приложение'

    )

    print('Токен валиден')

    print(f"Пользователь: {payload['name']}")

    print(f"Email: {payload['email']}")

    return payload

    except jwt.ExpiredSignatureError:

    print('Токен истек')

    return None

    except jwt.InvalidTokenError as e:

    print(f'Невалидный токен: {e}')

    return None

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

    токен = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'

    payload = проверить_токен(токен)

    if payload:

    print('Доступ разрешен')

    Middleware для Express

    Базовая защита роутов

    const jwt = require('jsonwebtoken');
    
    

    // Middleware для проверки JWT

    function аутентификация(req, res, next) {

    // Получить токен из заголовка

    const authHeader = req.headers['authorization'];

    const токен = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN

    if (!токен) {

    return res.status(401).json({

    ошибка: 'Токен не предоставлен'

    });

    }

    try {

    const payload = jwt.verify(токен, СЕКРЕТ);

    req.пользователь = payload; // Добавить данные пользователя в request

    next();

    } catch (err) {

    return res.status(403).json({

    ошибка: 'Невалидный или истекший токен'

    });

    }

    }

    // Использование в роутах

    const express = require('express');

    const app = express();

    // Публичный роут

    app.get('/api/публичные', (req, res) => {

    res.json({ сообщение: 'Публичные данные' });

    });

    // Защищенный роут

    app.get('/api/профиль', аутентификация, (req, res) => {

    res.json({

    пользователь: req.пользователь.name,

    email: req.пользователь.email

    });

    });

    // Роут только для админов

    function толькоАдмин(req, res, next) {

    if (req.пользователь.роль !== 'admin') {

    return res.status(403).json({

    ошибка: 'Требуются права администратора'

    });

    }

    next();

    }

    app.get('/api/админ', аутентификация, толькоАдмин, (req, res) => {

    res.json({ сообщение: 'Админ панель' });

    });

    Продвинутый middleware

    function создатьАутентификациюMiddleware(опции = {}) {
    

    const {

    секрет = process.env.JWT_SECRET,

    получитьТокен = стандартноеПолучениеТокена,

    обработкаОшибки = стандартнаяОбработкаОшибки

    } = опции;

    return function(req, res, next) {

    const токен = получитьТокен(req);

    if (!токен) {

    return обработкаОшибки(req, res, 'no_token');

    }

    jwt.verify(токен, секрет, (err, payload) => {

    if (err) {

    return обработкаОшибки(req, res, err.name);

    }

    req.пользователь = payload;

    next();

    });

    };

    }

    function стандартноеПолучениеТокена(req) {

    // Из заголовка Authorization

    const authHeader = req.headers['authorization'];

    if (authHeader) {

    return authHeader.split(' ')[1];

    }

    // Из cookie

    if (req.cookies && req.cookies.токен) {

    return req.cookies.токен;

    }

    // Из query параметра

    if (req.query.токен) {

    return req.query.токен;

    }

    return null;

    }

    function стандартнаяОбработкаОшибки(req, res, ошибка) {

    const статус = ошибка === 'no_token' ? 401 : 403;

    const сообщения = {

    'no_token': 'Токен не предоставлен',

    'TokenExpiredError': 'Токен истек',

    'JsonWebTokenError': 'Невалидный токен'

    };

    res.status(статус).json({

    ошибка: сообщения[ошибка] || 'Ошибка аутентификации'

    });

    }

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

    const аутентификация = создатьАутентификациюMiddleware();

    app.use('/api/защищенные', аутентификация);

    Flask (Python)

    Базовая защита

    from flask import Flask, request, jsonify
    

    from functools import wraps

    import jwt

    app = Flask(__name__)

    def требуется_токен(f):

    """

    Декоратор для защиты роутов

    """

    @wraps(f)

    def decorated(args, kwargs):

    токен = None

    # Получить токен из заголовка

    if 'Authorization' in request.headers:

    auth_header = request.headers['Authorization']

    токен = auth_header.split(' ')[1] if ' ' in auth_header else None

    if not токен:

    return jsonify({'ошибка': 'Токен не предоставлен'}), 401

    try:

    payload = jwt.decode(токен, СЕКРЕТ, algorithms=['HS256'])

    request.пользователь = payload

    except jwt.ExpiredSignatureError:

    return jsonify({'ошибка': 'Токен истек'}), 403

    except jwt.InvalidTokenError:

    return jsonify({'ошибка': 'Невалидный токен'}), 403

    return f(args, *kwargs)

    return decorated

    # Публичный роут

    @app.route('/api/публичные')

    def публичные():

    return jsonify({'сообщение': 'Публичные данные'})

    # Защищенный роут

    @app.route('/api/профиль')

    @требуется_токен

    def профиль():

    return jsonify({

    'пользователь': request.пользователь['name'],

    'email': request.пользователь['email']

    })

    # Роут только для админов

    def только_админ(f):

    @wraps(f)

    def decorated(args, *kwargs):

    if request.пользователь.get('роль') != 'admin':

    return jsonify({'ошибка': 'Требуются права администратора'}), 403

    return f(args, kwargs)

    return decorated

    @app.route('/api/админ')

    @требуется_токен

    @только_админ

    def админ():

    return jsonify({'сообщение': 'Админ панель'})

    Refresh Tokens

    Концепция

    • Access Token: Короткоживущий (15-30 минут)
    • Refresh Token: Долгоживущий (7-30 дней)

    Реализация

    function генерироватьТокены(пользователь) {
    

    // Access token - короткий

    const accessToken = jwt.sign(

    {

    sub: пользователь.id,

    name: пользователь.имя,

    роль: пользователь.роль

    },

    СЕКРЕТ,

    { expiresIn: '15m' }

    );

    // Refresh token - длинный

    const refreshToken = jwt.sign(

    {

    sub: пользователь.id,

    тип: 'refresh'

    },

    REFRESH_СЕКРЕТ,

    { expiresIn: '7d' }

    );

    return { accessToken, refreshToken };

    }

    // Роут для обновления токена

    app.post('/api/обновить', (req, res) => {

    const { refreshToken } = req.body;

    if (!refreshToken) {

    return res.status(401).json({ ошибка: 'Refresh токен отсутствует' });

    }

    try {

    const payload = jwt.verify(refreshToken, REFRESH_СЕКРЕТ);

    if (payload.тип !== 'refresh') {

    throw new Error('Неверный тип токена');

    }

    // Получить пользователя из БД

    const пользователь = получитьПользователяПоID(payload.sub);

    // Сгенерировать новые токены

    const токены = генерироватьТокены(пользователь);

    res.json(токены);

    } catch (err) {

    res.status(403).json({ ошибка: 'Невалидный refresh токен' });

    }

    });

    Алгоритмы подписи

    HMAC (Симметричный)

    // HS256, HS384, HS512
    

    const токен = jwt.sign(payload, 'секретный-ключ', {

    algorithm: 'HS256'

    });

    // Тот же ключ для проверки

    jwt.verify(токен, 'секретный-ключ');

    Плюсы:

    • Быстрый
    • Простой

    Минусы:

    • Один ключ для подписи и проверки
    • Ключ должен быть секретным

    RSA (Асимметричный)

    const fs = require('fs');
    
    

    // Приватный ключ для подписи

    const privateKey = fs.readFileSync('private.pem');

    const токен = jwt.sign(payload, privateKey, {

    algorithm: 'RS256'

    });

    // Публичный ключ для проверки

    const publicKey = fs.readFileSync('public.pem');

    jwt.verify(токен, publicKey);

    Плюсы:

    • Публичный ключ можно распространять
    • Только приватный ключ может подписывать

    Минусы:

    • Медленнее HMAC
    • Сложнее управление ключами

    Генерация RSA ключей

    # OpenSSL
    

    openssl genrsa -out private.pem 2048

    openssl rsa -in private.pem -pubout -out public.pem

    Безопасность JWT

    1. Храните секреты безопасно

    Плохо:

    const СЕКРЕТ = 'мой-секрет';

    Хорошо:

    const СЕКРЕТ = process.env.JWT_SECRET;
    

    if (!СЕКРЕТ) {

    throw new Error('JWT_SECRET не установлен');

    }

    2. Используйте короткое время жизни

    // Access token - короткий
    

    jwt.sign(payload, секрет, { expiresIn: '15m' });

    // Refresh token - длинный

    jwt.sign(payload, refresh_секрет, { expiresIn: '7d' });

    3. Проверяйте все claims

    jwt.verify(токен, секрет, {
    

    algorithms: ['HS256'], // Только разрешенные алгоритмы

    issuer: 'ваше-приложение',

    audience: 'пользователи'

    });

    4. Не храните чувствительные данные

    Плохо:

    const payload = {
    

    пароль: 'секрет123',

    номер_карты: '1234-5678-9012-3456'

    };

    Хорошо:

    const payload = {
    

    sub: пользователь.id,

    роль: пользователь.роль

    };

    5. Реализуйте blacklist для отзыва

    const redis = require('redis');
    

    const client = redis.createClient();

    async function отозватьТокен(токен) {

    const payload = jwt.decode(токен);

    const ttl = payload.exp - Math.floor(Date.now() / 1000);

    // Добавить в blacklist

    await client.setex(blacklist:${токен}, ttl, 'true');

    }

    async function проверитьBlacklist(токен) {

    const blacklisted = await client.get(blacklist:${токен});

    return blacklisted !== null;

    }

    // В middleware

    async function аутентификация(req, res, next) {

    const токен = получитьТокен(req);

    if (await проверитьBlacklist(токен)) {

    return res.status(403).json({ ошибка: 'Токен отозван' });

    }

    // ... обычная проверка ...

    }

    6. Защита от атак

    CSRF Protection:

    // Не полагайтесь только на cookies
    

    // Используйте Authorization заголовок

    res.setHeader('Authorization', Bearer ${токен});

    XSS Protection:

    // Не храните токены в localStorage
    

    // Используйте httpOnly cookies

    res.cookie('токен', токен, {

    httpOnly: true,

    secure: true, // Только HTTPS

    sameSite: 'strict'

    });

    Тестирование

    Unit тесты

    const jwt = require('jsonwebtoken');
    

    const { expect } = require('chai');

    describe('JWT функции', () => {

    const ТЕСТОВЫЙ_СЕКРЕТ = 'тестовый-секрет';

    it('должен создать валидный токен', () => {

    const payload = { sub: 123, name: 'Тест' };

    const токен = jwt.sign(payload, ТЕСТОВЫЙ_СЕКРЕТ);

    expect(токен).to.be.a('string');

    expect(токен.split('.')).to.have.lengthOf(3);

    });

    it('должен проверить токен', () => {

    const payload = { sub: 123, name: 'Тест' };

    const токен = jwt.sign(payload, ТЕСТОВЫЙ_СЕКРЕТ);

    const decoded = jwt.verify(токен, ТЕСТОВЫЙ_СЕКРЕТ);

    expect(decoded.sub).to.equal(123);

    expect(decoded.name).to.equal('Тест');

    });

    it('должен отклонить истекший токен', (done) => {

    const токен = jwt.sign(

    { sub: 123 },

    ТЕСТОВЫЙ_СЕКРЕТ,

    { expiresIn: '1ms' }

    );

    setTimeout(() => {

    try {

    jwt.verify(токен, ТЕСТОВЫЙ_СЕКРЕТ);

    done(new Error('Должна была быть ошибка'));

    } catch (err) {

    expect(err.name).to.equal('TokenExpiredError');

    done();

    }

    }, 10);

    });

    });

    Отладка JWT

    Декодирование токена

    // Декодировать без проверки подписи
    

    const decoded = jwt.decode(токен, { complete: true });

    console.log('Header:', decoded.header);

    console.log('Payload:', decoded.payload);

    console.log('Signature:', decoded.signature);

    Онлайн инструменты

    • jwt.io: Декодирование и отладка токенов
    • jwt-debugger*: Проверка подписи

    Логирование

    function логироватьТокен(токен) {
    

    const decoded = jwt.decode(токен);

    console.log('Токен информация:');

    console.log(' Пользователь:', decoded.sub);

    console.log(' Издан:', new Date(decoded.iat 1000).toLocaleString());

    console.log(' Истекает:', new Date(decoded.exp * 1000).toLocaleString());

    console.log(' Осталось:', Math.floor((decoded.exp - Date.now() / 1000) / 60), 'минут');

    }

    Заключение

    JWT — мощный инструмент для аутентификации и авторизации в современных приложениях. Ключевые выводы:

    Используйте:

    • Короткое время жизни access токенов
    • Refresh токены для продления сессии
    • Безопасное хранение секретов
    • HTTPS для передачи токенов

    Избегайте:

    • Хранения чувствительных данных в payload
    • Длинного времени жизни access токенов
    • Слабых секретных ключей
    • Хранения токенов в localStorage (XSS риск)

    JWT — это не серебряная пуля, но при правильном использовании он обеспечивает надежную и масштабируемую систему аутентификации!

    Share:

    Похожие статьи

    Read in English