JWT (JSON Web Token): плюсы, минусы и основные риски безопасности

Ответ

JWT — это открытый стандарт (RFC 7519) для создания токенов доступа, которые используются для безопасной передачи информации между сторонами в виде JSON-объекта. Токен состоит из трех частей: заголовок (Header), полезная нагрузка (Payload) и подпись (Signature).

Плюсы:

  • Stateless (отсутствие состояния на сервере): Серверу не нужно хранить информацию о сессиях. Вся необходимая информация (ID пользователя, роли) содержится в самом токене. Это упрощает архитектуру.
  • Масштабируемость: Благодаря stateless-подходу, запросы с одним и тем же токеном могут обрабатываться любым экземпляром сервиса, что идеально для микросервисной архитектуры и балансировки нагрузки.
  • Целостность данных: Подпись (Signature) гарантирует, что данные в токене не были изменены в пути. Сервер, зная секретный ключ, может проверить подлинность токена.
  • Кросс-доменность (CORS): JWT легко передавать в заголовках HTTP, что делает его удобным для взаимодействия между разными доменами и сервисами (например, веб-клиент на site.com и API на api.site.com).

Минусы и риски:

  • Невозможность принудительного отзыва: Главный недостаток. Если токен украден, он будет действителен до истечения своего срока (exp). Решение: использовать короткоживущие access-токены (5-15 минут) и долгоживущие refresh-токены для их обновления.
  • Размер: JWT обычно больше, чем ID сессии в cookie, так как содержит данные. Это может незначительно увеличить трафик, особенно при передаче с каждым запросом.
  • Безопасность хранения на клиенте:
    • localStorage: Уязвим для XSS-атак (любой скрипт на странице может получить доступ к токену).
    • Cookies: Более безопасный вариант, если использовать флаг HttpOnly (недоступен из JavaScript) и Secure (передача только по HTTPS).
  • Данные в Payload не зашифрованы: Содержимое полезной нагрузки (Payload) просто закодировано в Base64Url и легко читается кем угодно. Никогда не храните в JWT чувствительные данные (пароли, личную информацию)! Для шифрования используется другой стандарт — JWE (JSON Web Encryption).
// Пример создания JWT с использованием библиотеки github.com/golang-jwt/jwt
import (
    "time"
    "github.com/golang-jwt/jwt/v4"
)

// Секретный ключ должен храниться безопасно, например, в переменных окружения
var jwtKey = []byte("my_secret_key")

func createToken(userID int) (string, error) {
    // Устанавливаем срок жизни токена
    expirationTime := time.Now().Add(15 * time.Minute)

    // Создаем полезную нагрузку (claims)
    claims := &jwt.RegisteredClaims{
        Subject:   string(userID),
        ExpiresAt: jwt.NewNumericDate(expirationTime),
    }

    // Создаем токен с указанием алгоритма подписи и claims
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

    // Подписываем токен нашим секретным ключом
    return token.SignedString(jwtKey)
}

Ответ 18+ 🔞

Так, слушай, про эти ваши JWT. Сижу тут, думаю, ну что за мода пошла — все как с ума посходили, везде их суют, прям панацея какая-то. А на деле-то, блядь, как всегда, не всё так радужно, ёпта.

Ну, JWT — это, грубо говоря, такая бумажка, билетик. Состоит из трёх кусков, склеенных точками. Первый — заголовок, там написано, какой алгоритм подписи. Второй — полезняк, там твой ID, роли, срок годности. Третий — подпись, чтобы не подделали. Всё это в JSON заворачивается и кодируется. Вроде бы красиво.

Чем хороши, эти ваши билетики?

  • Stateless, ёбта! Серверу не нужно помнить, кто ты такой. Всё про тебя написано прямо в токене. Пришёл — показал билетик — тебя опознали. Серверу не нужно заводить на тебя папочку в памяти или в базе. Архитектура проще, голова не болит.
  • Масштабируется, как сумасшедший. Раз серверу нихуя не надо помнить, то хоть тысяча серверов тебя примут. Кинул запрос на любой — он токен проверил и всё. Для микросервисов — просто песня.
  • Не подделаешь. Подпись-то проверяется секретным ключом. Попробуй что-то в полезной нагрузке поменять — подпись не сойдётся, и тебя, пидораса шерстяного, нахуй пошлют.
  • Междоменный. Кинул его в заголовок Authorization: Bearer <токен> — и всё, лети себе между frontend.com и api.backend.com. CORS только разреши.

А теперь, блядь, ложка дёгтя, размером с лопату. Минусы, на которых все обжигаются.

  • Главная жопа — отозвать его нихуя нельзя. Украли у тебя токен? Ну всё, пиздец. Он будет работать, пока не истечёт срок. Это как потерять пропуск на завод — любой найдёт и пройдёт. Что делать? Делать токены короткоживущими, на 5-15 минут. А для обновления — отдельный, долгоживущий refresh-токен, который хранишь как зеницу ока и можешь отозвать в случае чего.
  • Толстый он, сука. Не сравнить с какой-нибудь сессионной кукой. Тащить его в каждом запросе — трафик чуть подрастёт.
  • Где хранить-то, мудя?
    • В localStorage? Да ты охренел? Любой XSS-скрипт на странице вытащит его и унесёт в закат. НИКОГДА.
    • В куках с HttpOnly и Secure? Вот это уже умнее. Из JS не достанешь, и летит только по HTTPS. Но тут свои танцы с CORS и CSRF начинаются. В общем, думай, блядь, голова дана не только для еды.
  • Данные не шифрованы, ёпта! Полезная нагрузка просто в Base64 закодирована. Раскодируй — и читай на здоровье. НИКОГДА НЕ ПИХАЙ ТУДА ПАРОЛИ, НОМЕРА КАРТОЧЕК И ПРОЧУЮ ХУЙНЮ! Это не для секретов. Хочешь секреты — смотри в сторону JWE.
// Вот, смотри, как на Go эту хрень генерить. Библиотека github.com/golang-jwt/jwt
import (
    "time"
    "github.com/golang-jwt/jwt/v5" // версия-то новая, блядь, v5
)

// Секретный ключ. Спрячь его, идиот! В переменные окружения, в спец. хранилище — куда угодно, только не в код.
var jwtKey = []byte("мой_супер_пупер_секрет_который_никто_не_угадает")

func слепитьТокен(userID int) (string, error) {
    // Срок годности ставим. 15 минут — и в утиль.
    expirationTime := time.Now().Add(15 * time.Minute)

    // Полезная нагрузка. Subject — обычно ID юзера.
    claims := &jwt.RegisteredClaims{
        Subject:   fmt.Sprintf("%d", userID), // Переводим int в string, а то обосрёшься
        ExpiresAt: jwt.NewNumericDate(expirationTime),
    }

    // Лепим токен с алгоритмом HS256.
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

    // Подписываем своим ключом. Всё, готово, неси на кассу.
    return token.SignedString(jwtKey)
}

Вот и вся магия. Инструмент, в общем, мощный, но если им как молотком гвозди забивать — себе же пальцы отобьёшь. Думай, где и как применяешь, и про безопасность не забывай, а то охуеешь потом от последствий.