JWT (JSON Web Tokens): Полное руководство

Введение: Что такое JWT и зачем он нужен?

JSON Web Token (JWT), произносится как “джот” (/dʒɒt/), представляет собой открытый стандарт (RFC 7519) для создания токенов доступа, которые используются для безопасной передачи информации между двумя сторонами в виде JSON-объекта. В мире веб-разработки JWT стали де-факто стандартом для реализации аутентификации и авторизации в API, особенно в микросервисной архитектуре и одностраничных приложениях (SPA).

Основная идея JWT заключается в создании “самодостаточного” токена. Это означает, что вся необходимая информация для проверки пользователя (например, его идентификатор и права доступа) содержится внутри самого токена. Когда клиент отправляет запрос на сервер, он прикрепляет к нему этот токен. Сервер, получив токен, может проверить его подлинность и, если проверка прошла успешно, предоставить доступ к запрашиваемому ресурсу. Такой подход избавляет сервер от необходимости хранить информацию о сессиях в базе данных, что упрощает масштабирование и повышает производительность системы.

Основные концепции: Структура JWT

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

xxxxx.yyyyy.zzzzz

  1. Заголовок (Header)
  2. Полезная нагрузка (Payload)
  3. Подпись (Signature)

Давайте рассмотрим каждую часть подробнее.

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

Заголовок — это JSON-объект, который содержит метаданные о токене. Как правило, он состоит из двух полей:

  • typ (Type): Тип токена, который всегда имеет значение “JWT”.
  • *alg(Algorithm): Алгоритм хеширования, используемый для создания подписи. Наиболее распространенными являютсяHS256(HMAC с использованием SHA-256) иRS256` (RSA с использованием SHA-256).

Пример заголовка:

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

Этот JSON-объект кодируется в строку формата Base64Url и образует первую часть JWT.

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

Полезная нагрузка — это JSON-объект, содержащий так называемые “заявки” (claims). Заявки — это утверждения о сущности (обычно о пользователе) и дополнительные данные. Существует три типа заявок:

  • Зарезервированные (Registered claims): Это стандартные, предопределенные заявки, которые не являются обязательными, но рекомендуются к использованию. Некоторые из них:

  • iss (Issuer): Идентификатор издателя токена.

  • sub (Subject): Идентификатор “темы” токена (например, ID пользователя).

  • aud (Audience): Идентификаторы получателей токена.

  • exp (Expiration Time): Время истечения срока действия токена (в формате Unix time).

    • iat (Issued At): Время создания токена (в формате Unix time).).
  • Публичные (Public claims): Эти заявки определяются теми, кто использует JWT. Чтобы избежать коллизий, их имена должны быть уникальными. Обычно для этого используют URI.

  • Приватные (Private claims): Это пользовательские заявки, созданные для обмена информацией между сторонами, которые договорились об их использовании. Они не являются ни зарезервированными, ни публичными.

Пример полезной нагрузки:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true,
  "iat": 1516239022
}

Важно: Полезная нагрузка, как и заголовок, кодируется в Base64Url. Это означает, что любой, кто перехватит токен, сможет легко прочитать его содержимое. Поэтому никогда не храните в полезной нагрузке конфиденциальную информацию, такую как пароли.

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

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

Псевдокод для создания подписи с использованием HS256:

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

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

Практические примеры

Пример 1: Схема аутентификации

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

sequenceDiagram
    participant User
    participant AppServer
    participant AuthServer

    User->>AuthServer: Запрос на аутентификацию (логин/пароль)
    AuthServer->>AuthServer: Проверка учетных данных
    alt Успешная аутентификация
        AuthServer->>AuthServer: Создание JWT
        AuthServer-->>User: Возврат JWT
    else Неудачная аутентификация
        AuthServer-->>User: Ошибка аутентификации
    end

    User->>AppServer: Запрос к защищенному ресурсу + JWT в заголовке Authorization
    AppServer->>AppServer: Проверка подписи и срока действия JWT
    alt Валидный токен
        AppServer-->>User: Возврат запрошенного ресурса
    else Невалидный токен
        AppServer-->>User: Ошибка 401 Unauthorized
    end

Пример 2: Создание и проверка JWT на Python

Для работы с JWT в Python можно использовать библиотеку PyJWT. Сначала установим ее:

pip install PyJWT

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

import jwt
import datetime

# Секретный ключ (в реальном приложении должен быть сложным и храниться в секрете)
SECRET_KEY = "your-super-secret-key"

# Данные для полезной нагрузки
payload = {
    "user_id": 123,
    "username": "testuser",
    "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1), # Срок жизни - 1 час
    "iat": datetime.datetime.utcnow()
}

# Создание JWT
token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")

print(f"Сгенерированный токен: {token}")

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

import jwt

# Предположим, это токен, полученный от клиента
received_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjMsInVzZXJuYW1lIjoidGVzdHVzZXIiLCJleHAiOjE2NzQ0ODQyMjMsImlhdCI6MTY3NDQ4MDYyM30.some_signature"
SECRET_KEY = "your-super-secret-key"

try:
    # Декодирование и проверка токена
    decoded_payload = jwt.decode(received_token, SECRET_KEY, algorithms=["HS256"])
    print("Токен валиден!")
    print(f"Полезная нагрузка: {decoded_payload}")

except jwt.ExpiredSignatureError:
    print("Срок действия токена истек.")
except jwt.InvalidTokenError:
    print("Невалидный токен.")

Типичные ошибки и как их избежать

  1. Х1.Хранение токенов в localStorage:** Это делает их уязвимыми для XSS-атак. Если злоумышленник сможет выполнить свой JavaScript-код на вашей странице, он получит доступ к токену. Более безопасной альтернативой является хранение токенов в HttpOnly cookies. Это предотвращает доступ к ним из JavaScript.
  2. 2.Использование слабых секретных ключей:** Секретный ключ должен быть длинным, сложным и случайным. Не используйте очевидные строки вроде “secret” или “password”.
  3. 3.Отсутствие срока действия (exp):** Токены без срока действия могут быть использованы злоумышленником бесконечно, если они будут скомпрометированы. Всегда устанавливайте короткий срок жизни для токенов доступа (access tokens) и используйте токены обновления (refresh tokens) для получения новых.
  4. П4.Передача конфиденциальной информации в payload:** Как уже упоминалось, payload не шифруется. Не храните в нем пароли, личные данные или другую чувствительную информацию.

Связь с другими темами

JWT часто используется в связке с OAuth 2.0. В этом сценарии JWT может выступать в роли токена доступа (access token). Когда клиентское приложение получает от сервера авторизации токен доступа в формате JWT, оно может использовать его для аутентификации запросов к серверу ресурсов. Сервер ресурсов, в свою очередь, может проверить токен локально, без обращения к серверу авторизации, что является одним из ключевых преимуществ использования JWT.

Заключение

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