JWT токены для API: полное руководство

← Вернуться к статьям

Введение

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

В этом руководстве мы разберем JWT от основ до практической реализации: структуру токена, создание и валидацию, работу с refresh tokens, безопасность и best practices для использования JWT в API.

✅ Что вы узнаете:

📋 Содержание

Что такое JWT? 🔐

JWT (JSON Web Token) — это компактный и самодостаточный способ безопасной передачи информации между сторонами. JWT состоит из трех частей, разделенных точками: header.payload.signature

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

Как работает JWT?

Процесс работы с JWT:

Шаг 1: Пользователь авторизуется

Пользователь отправляет учетные данные (логин/пароль) на сервер.

Шаг 2: Сервер создает JWT

Сервер проверяет учетные данные и создает JWT токен с информацией о пользователе.

Шаг 3: Клиент получает токен

Сервер отправляет JWT токен клиенту (обычно в ответе на запрос авторизации).

Шаг 4: Клиент отправляет токен

Клиент сохраняет токен и отправляет его в заголовке Authorization: Bearer <token> при каждом запросе к API.

Шаг 5: Сервер валидирует токен

Сервер проверяет подпись токена и извлекает информацию из payload без обращения к базе данных.

Структура JWT 🧩

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

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Три части JWT токена:

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

Содержит метаданные о токене: тип токена (JWT) и алгоритм подписи (например, HS256, RS256).

{
  "alg": "HS256",
  "typ": "JWT"
}

После Base64URL кодирования: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

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

Содержит claims (утверждения) — информацию о пользователе и метаданные токена.

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022,
  "exp": 1516242622
}

После Base64URL кодирования: eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

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

Создается путем подписи закодированных header и payload секретным ключом.

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

Результат: SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Стандартные Claims (JWT Claims)

Claim Описание Пример
iss (issuer) Кто выдал токен "https://api.example.com"
sub (subject) Идентификатор пользователя "1234567890"
aud (audience) Для кого предназначен токен "my-api"
exp (expiration) Время истечения токена (Unix timestamp) 1516242622
iat (issued at) Время создания токена 1516239022
nbf (not before) Токен недействителен до этого времени 1516239022
jti (JWT ID) Уникальный идентификатор токена "550e8400-e29b-41d4-a716-446655440000"

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

Node.js (jsonwebtoken)

// create-jwt.js
const jwt = require('jsonwebtoken');

// Секретный ключ (в продакшене храните в переменных окружения)
const SECRET_KEY = 'your-secret-key';

// Создание JWT токена
function createToken(user) {
  const payload = {
    sub: user.id,
    name: user.name,
    email: user.email,
    role: user.role,
    iat: Math.floor(Date.now() / 1000), // Текущее время
    exp: Math.floor(Date.now() / 1000) + (60 * 60) // Истекает через 1 час
  };

  const token = jwt.sign(payload, SECRET_KEY, {
    algorithm: 'HS256',
    expiresIn: '1h'
  });

  return token;
}

// Пример использования
const user = {
  id: '1234567890',
  name: 'John Doe',
  email: 'john@example.com',
  role: 'user'
};

const token = createToken(user);
console.log('JWT Token:', token);

// Результат:
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZW1haWwiOiJqb2huQGV4YW1wbGUuY29tIiwicm9sZSI6InVzZXIiLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTUxNjI0MjYyMn0.xxx

Python (PyJWT)

# create_jwt.py
import jwt
import datetime

SECRET_KEY = 'your-secret-key'

def create_token(user):
    payload = {
        'sub': user['id'],
        'name': user['name'],
        'email': user['email'],
        'role': user['role'],
        'iat': datetime.datetime.utcnow(),
        'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
    }
    
    token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
    return token

# Пример использования
user = {
    'id': '1234567890',
    'name': 'John Doe',
    'email': 'john@example.com',
    'role': 'user'
}

token = create_token(user)
print(f'JWT Token: {token}')

PHP (firebase/php-jwt)

// create_jwt.php
<?php
require 'vendor/autoload.php';

use Firebase\JWT\JWT;
use Firebase\JWT\Key;

$secretKey = 'your-secret-key';

function createToken($user) {
    global $secretKey;
    
    $payload = [
        'sub' => $user['id'],
        'name' => $user['name'],
        'email' => $user['email'],
        'role' => $user['role'],
        'iat' => time(),
        'exp' => time() + (60 * 60) // 1 час
    ];
    
    $token = JWT::encode($payload, $secretKey, 'HS256');
    return $token;
}

// Пример использования
$user = [
    'id' => '1234567890',
    'name' => 'John Doe',
    'email' => 'john@example.com',
    'role' => 'user'
];

$token = createToken($user);
echo "JWT Token: " . $token;
?>

Валидация JWT токенов ✅

Валидация JWT токена включает проверку подписи, срока действия и других claims.

Node.js

// validate-jwt.js
const jwt = require('jsonwebtoken');
const express = require('express');
const app = express();

const SECRET_KEY = 'your-secret-key';

// Middleware для проверки JWT
function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN

  if (!token) {
    return res.status(401).json({ error: 'Токен не предоставлен' });
  }

  jwt.verify(token, SECRET_KEY, (err, decoded) => {
    if (err) {
      if (err.name === 'TokenExpiredError') {
        return res.status(401).json({ error: 'Токен истек' });
      }
      if (err.name === 'JsonWebTokenError') {
        return res.status(401).json({ error: 'Недействительный токен' });
      }
      return res.status(403).json({ error: 'Ошибка проверки токена' });
    }

    req.user = decoded; // Добавляем информацию о пользователе в запрос
    next();
  });
}

// Защищенный endpoint
app.get('/api/user', authenticateToken, (req, res) => {
  res.json({
    message: 'Доступ разрешен',
    user: req.user
  });
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

Python (Flask)

# validate_jwt.py
from flask import Flask, request, jsonify
import jwt
from functools import wraps

app = Flask(__name__)
SECRET_KEY = 'your-secret-key'

def token_required(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        token = None
        
        if 'Authorization' in request.headers:
            auth_header = request.headers['Authorization']
            try:
                token = auth_header.split(' ')[1]  # Bearer TOKEN
            except IndexError:
                return jsonify({'error': 'Неверный формат токена'}), 401
        
        if not token:
            return jsonify({'error': 'Токен не предоставлен'}), 401
        
        try:
            decoded = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
            request.user = decoded
        except jwt.ExpiredSignatureError:
            return jsonify({'error': 'Токен истек'}), 401
        except jwt.InvalidTokenError:
            return jsonify({'error': 'Недействительный токен'}), 401
        
        return f(*args, **kwargs)
    
    return decorated

@app.route('/api/user', methods=['GET'])
@token_required
def get_user():
    return jsonify({
        'message': 'Доступ разрешен',
        'user': request.user
    })

if __name__ == '__main__':
    app.run(port=5000)

Refresh Tokens 🔄

Refresh Token — это долгоживущий токен, который используется для получения новых access tokens без повторной авторизации пользователя.

✅ Преимущества Refresh Tokens:

Реализация Refresh Tokens

// refresh-tokens.js
const jwt = require('jsonwebtoken');
const crypto = require('crypto');

const ACCESS_TOKEN_SECRET = 'access-secret-key';
const REFRESH_TOKEN_SECRET = 'refresh-secret-key';

// Создание пары токенов
function createTokenPair(user) {
  // Access Token (короткоживущий)
  const accessToken = jwt.sign(
    {
      sub: user.id,
      name: user.name,
      role: user.role
    },
    ACCESS_TOKEN_SECRET,
    { expiresIn: '15m' }
  );

  // Refresh Token (долгоживущий)
  const refreshToken = jwt.sign(
    {
      sub: user.id,
      type: 'refresh'
    },
    REFRESH_TOKEN_SECRET,
    { expiresIn: '7d' }
  );

  return { accessToken, refreshToken };
}

// Обновление access token
function refreshAccessToken(refreshToken) {
  try {
    // Проверяем refresh token
    const decoded = jwt.verify(refreshToken, REFRESH_TOKEN_SECRET);
    
    // Проверяем, что токен не в черном списке (в реальном приложении)
    // if (await isTokenBlacklisted(refreshToken)) {
    //   throw new Error('Токен отозван');
    // }
    
    // Создаем новый access token
    const user = { id: decoded.sub };
    const newAccessToken = jwt.sign(
      {
        sub: user.id,
        name: user.name,
        role: user.role
      },
      ACCESS_TOKEN_SECRET,
      { expiresIn: '15m' }
    );
    
    return newAccessToken;
  } catch (error) {
    throw new Error('Недействительный refresh token');
  }
}

// Пример использования
app.post('/api/auth/refresh', async (req, res) => {
  const { refreshToken } = req.body;
  
  if (!refreshToken) {
    return res.status(401).json({ error: 'Refresh token не предоставлен' });
  }
  
  try {
    const newAccessToken = refreshAccessToken(refreshToken);
    res.json({ accessToken: newAccessToken });
  } catch (error) {
    res.status(401).json({ error: error.message });
  }
});

Реализация на разных языках 💻

Go (github.com/golang-jwt/jwt)

// jwt.go
package main

import (
    "fmt"
    "time"
    "github.com/golang-jwt/jwt/v5"
)

var secretKey = []byte("your-secret-key")

type Claims struct {
    UserID string `json:"sub"`
    Name   string `json:"name"`
    Role   string `json:"role"`
    jwt.RegisteredClaims
}

func CreateToken(userID, name, role string) (string, error) {
    claims := Claims{
        UserID: userID,
        Name:   name,
        Role:   role,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
        },
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(secretKey)
}

func ValidateToken(tokenString string) (*Claims, error) {
    token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
        return secretKey, nil
    })

    if err != nil {
        return nil, err
    }

    if claims, ok := token.Claims.(*Claims); ok && token.Valid {
        return claims, nil
    }

    return nil, fmt.Errorf("недействительный токен")
}

Java (io.jsonwebtoken:jjwt)

// JwtUtil.java
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.util.Date;

public class JwtUtil {
    private static final String SECRET_KEY = "your-secret-key-which-must-be-at-least-256-bits";
    private static final Key key = Keys.hmacShaKeyFor(SECRET_KEY.getBytes());
    
    public static String createToken(String userId, String name, String role) {
        return Jwts.builder()
            .setSubject(userId)
            .claim("name", name)
            .claim("role", role)
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + 3600000)) // 1 час
            .signWith(key, SignatureAlgorithm.HS256)
            .compact();
    }
    
    public static Claims validateToken(String token) {
        return Jwts.parserBuilder()
            .setSigningKey(key)
            .build()
            .parseClaimsJws(token)
            .getBody();
    }
}

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

Безопасность JWT критически важна. Вот основные рекомендации:

✅ Best Practices безопасности:

⚠️ Распространенные уязвимости:

Защита от Algorithm Confusion

// Безопасная валидация JWT
const jwt = require('jsonwebtoken');

function validateToken(token) {
  // ВАЖНО: Всегда явно указывайте алгоритм
  const decoded = jwt.verify(token, SECRET_KEY, {
    algorithms: ['HS256'] // Явно указываем алгоритм
  });
  
  // Проверяем все необходимые claims
  if (!decoded.exp || decoded.exp < Math.floor(Date.now() / 1000)) {
    throw new Error('Токен истек');
  }
  
  if (!decoded.sub) {
    throw new Error('Отсутствует subject');
  }
  
  return decoded;
}

// НЕПРАВИЛЬНО - уязвимо к algorithm confusion
function unsafeValidateToken(token) {
  // Не указывает алгоритм - уязвимо!
  return jwt.verify(token, SECRET_KEY);
}

Где хранить JWT токены?

Место хранения Безопасность Когда использовать
httpOnly Cookies ✅ Высокая Веб-приложения (защита от XSS)
Память (JavaScript) ✅ Высокая SPA приложения
Secure Storage ✅ Высокая Мобильные приложения (Keychain/Keystore)
localStorage ❌ Низкая Не рекомендуется (уязвимо к XSS)
sessionStorage ❌ Низкая Не рекомендуется (уязвимо к XSS)

Best Practices 📚

✅ Рекомендации по использованию JWT:

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

✅ JWT подходит для:

❌ JWT НЕ подходит для:

Заключение

JWT токены — это мощный инструмент для аутентификации в современных API. При правильной реализации они обеспечивают безопасность, масштабируемость и удобство использования.

💡 Ключевые выводы:

Создайте Mock API с JWT аутентификацией за 2 минуты

Хотите протестировать JWT интеграцию? Создайте Mock API с помощью LightBox API и попробуйте различные сценарии аутентификации без необходимости настраивать сложный backend.

Попробовать бесплатно →
← Вернуться к статьям