» »
Работа с JWT в Golang
#Программирование

Работа с JWT в Golang

Почти каждый веб-проект сегодня требует аутентификации. Будь то блог, маркетплейс или внутренняя CRM – пользователь должен входить в систему, а сервер – уверенно понимать, кто перед ним. Здесь на сцену выходит JWT. Компактный, самодостаточный и... потенциально опасный, если обращаться с ним неумело.

Анна М.
0
83
12 мин

Введение

Работа с JWT в Golang даёт нужный баланс между скоростью, безопасностью и удобством. Но только если понимать, как устроен токен, как его подписывать, проверять и использовать (например, в middleware). В противном случае можно легко открыть дверь для атак и проблем, которых «можно было избежать».

Погружаемся в тему

Вы наверняка слышали про JWT. JSON Web Token – давно популярная технология и её значение продолжает расти. Особенно в распределенных архитектурах.

Разберём, как именно работает jwt token, научимся парсить и валидировать его, подключать авторизацию через middleware и – что особенно важно – избегать типичных ошибок. Всё с примерами, практикой и ссылками на рабочие библиотеки, включая https://github.com/golang-jwt/jwt.

Даже если вы только начали осваивать Go, вам будет понятно: от простого примера до рабочего кода на сервере. Если уже работали с JWT в других языках – увидите, почему Golang делает это иначе а, возможно, лучше.

При этом путь изучения можно ускорить. Например, на курсе «Искусство работы с ошибками и безмолвной паники» кроме всего прочего вы узнаете, как обрабатывать ошибки грамотно, не ломая логику приложения.

А теперь к сути. Сначала разберёмся, что такое JWT, из чего он состоит и как работает под капотом.

Что такое JSON Web Token (JWT)

JWT – это способ безопасно передавать данные между сторонами в формате JSON-объекта. Главная особенность: самодостаточность токена и его stateless природа. Сервер не хранит сессии – достаточно проверить подпись и извлечь полезную информацию из payload.

Как устроен JWT

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

  1. Header – указывает тип токена и алгоритм подписи (чаще всего RSA или на худой конец HMAC).
  2. Payload – содержит claims: идентификаторы, роли, срок действия и прочие данные.
  3. Signature – создаётся на основе первых двух частей и секретного ключа.

Пример:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

На практике клиент отправляет JWT с каждым запросом, обычно в заголовке:

Authorization: Bearer <jwt-token>

Сервер при получении:

  • Проверяет подпись
  • Смотрит на срок действия
  • Извлекает информацию о пользователе

P.S. Стандартом для создания тестовых токенов и их проверки является сайт jwt.io.

Где применяют JWT

Access и refresh токены, реализованные через jwt, активно используют в:

  • REST API: простое и независимое решение
  • SPA (Single Page Application): хранение на клиенте, авторизация через JS
  • Микросервисах: кросс-сервисная аутентификация и авторизация между сервисами
  • Мобильных приложениях: постоянный доступ к API без логина при каждом запуске

Если сравнивать с куками и сессионным хранилищем, JWT выигрывает в скорости и отказоустойчивости. Но только если выстроена правильная проверка.

Где подстерегают ошибки

JWT может легко стать слабым звеном в безопасности, если:

  • Не проверяется алгоритм подписи
  • Не валидируется срок действия (exp)
  • Разрешён alg: none
  • Используется предсказуемый или жёстко захардкоженный секрет

Даже при использовании популярной библиотеки github.com/golang-jwt/jwt важно не терять внимательность. Простая опечатка или игнорирование err при разборе токена может привести к критическим уязвимостям.

Атака через alg: none

В начале 2010-х многие библиотеки JWT допускали токены без подписи, если в заголовке был указан alg: none. Злоумышленник мог сконструировать токен с произвольными payload и header, указать alg: none и обойти валидацию.

{

  "alg": "none",

  "typ": "JWT"

}

.

{

  "sub": "admin",

  "exp": 9999999999

}

 

Если бэкенд не проверяет alg, библиотека может интерпретировать это как «валидный» токен, даже без подписи.

Как защититься:

  • Никогда не доверяйте alg из заголовка токена.
  • Жёстко фиксируйте допустимый алгоритм в коде:

token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (any, error) {

    if _, ok := token.Method.(*jwt.SigningMethodRS256); !ok {

        return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])

    }

    return []byte(secret), nil

})

 

Даже если библиотека по умолчанию не поддерживает alg: none, полагаться на это не стоит. Лучше контролировать поведение явно.

Как парсить JWT token в Go

Парсинг JWT в Go – не просто формальность. Малейшая ошибка валидации может привести к уязвимости. Особенно если разработчик слепо доверяет содержимому токена. Ниже – практика на базе популярной библиотеки https://github.com/golang-jwt/jwt.

Пример разбора токена

Предположим, клиент прислал JWT в заголовке Authorization. Разбираем и валидируем токен:

secret := []byte("your_secret_key")

 

token, err := jwt.Parse(authTokenStr, func(token *jwt.Token) (any, error) {

    if _, ok := token.Method.(*jwt.SigningMethodRS256); !ok {

        return nil, fmt.Errorf("unexpected signing algorithm: %v", token.Header["alg"])

    }

    return secret, nil

})

 

if err != nil {

    log.Println("JWT parsing error:", err)

    return

}

 

if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {

    fmt.Println("User ID:", claims["sub"])

} else {

    log.Println("Invalid token")

}

 

Что важно:

  • Обязательно проверяйте алгоритм подписи – если этого не делать, можно принять поддельный токен.
  • Ошибки при разборе нужно обрабатывать – никогда не полагайтесь только на факт наличия токена – об этом далее.

 

Как интерпретировать ошибки

Типичные ошибки при разборе:

  • Подпись невалидна
  • Токен просрочен (exp)
  • Неверный формат кодировки

Можно уточнить, что именно пошло не так:

var verr *jwt.ValidationError

if errors.As(err, &verr) {

    if verr.Errors&jwt.ValidationErrorExpired != 0 {

        log.Println("Token expired")

    }

    if verr.Errors&jwt.ValidationErrorSignatureInvalid != 0 {

        log.Println("Invalid token signature")

    }

}

Заходите на курс «Искусство работы с ошибками и безмолвной паники», чтобы стать настоящими экспертами errors.As и не только :)

Частые ошибки при работе с JWT

Вот типичные промахи, которые можно встретить даже в продакшене:

  • Проверяют только token != nil, игнорируя err из Parse() – в результате могут пропустить некорректный токен.
  • Не валидируют exp (время жизни) – просроченные токены продолжают "жить".
  • Хранят чувствительные данные в payload, включая e-mail или ФИО пользователя. Всё это легко декодируется без ключа, ведь JWT – это не шифрование, а просто Base64.

Пример уязвимости при неправильной проверке alg

Рассмотрим интересную атаку, возможную при использовании JWT с динамической обработкой алгоритма:

  1. Сервер генерирует токены с alg: RS256 (RSA) и проверяет их с помощью публичного ключа.
  2. Клиент (или злоумышленник) формирует свой токен, подписывая его утекшим тем же публичным ключом, но меняет алгоритм на HS256. ({“alg”: “HS256”, “typ”: “JWT”}).
  3. Если сервер доверяет alg, указанному в заголовке токена, он может интерпретировать RSA public key как HMAC secret и успешно проверить подпись злоумышленника.

Такая ошибка возможна, если библиотека JWT не ограничивает допустимые алгоритмы или разработчик сам не зафиксировал alg в конфигурации.

Что делать:

  • Всегда жёстко задавайте алгоритм проверки, игнорируя значение из заголовка токена.
  • Никогда не используйте token.Header["alg"] для определения способа валидации.
  • При возможности – предпочитайте асимметричные алгоритмы (RS256, ES256) HMAC'у в публичных API.

Далее – как встроить JWT в серверную авторизацию.

JWT-авторизация на сервере с Golang

Добавить авторизацию через JWT в Go – не просто прикрутить пару строк. Нужно грамотно встроить валидацию токена в middleware, обеспечить проверку подписи, срока действия и передавать данные в хендлеры без костылей. Разберём, как это делается на практике.

Что такое middleware

Middleware – это прослойка между запросом клиента и обработкой на сервере. Именно здесь лучше всего проверять JWT: если токен невалиден, то дальше выполнение не идёт.

Пример базового middleware:

package auth

 

import (

            "context"

            "fmt"

            “log/slog”

            "net/http"

            "strings"

 

            "github.com/golang-jwt/jwt/v5"

)

 

const authPrefix = “Bearer “

 

func JWTMiddleware(secretKey []byte) func(http.Handler) http.Handler {

            return func(next http.Handler) http.Handler {

                        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

                                    authHeader := r.Header.Get("Authorization")

                                    if authHeader == "" || !strings.HasPrefix(authHeader, authPrefix) {

                                               http.Error(w, "Missing or invalid Authorization header", http.StatusUnauthorized)

                                               return

                                    }

 

                                    tokenStr := strings.TrimPrefix(authHeader, authPrefix)

 

                                    token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (any, error) {

                                               if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {

                                                           return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])

                                               }

                                               return secretKey, nil

                                    })

 

                                    if err != nil || !token.Valid {

                                               http.Error(w, "Invalid token", http.StatusUnauthorized)

                                               return

                                    }

 

                                    ctx := TokenToContext(r.Context(), token)

                                    next.ServeHTTP(w, r.WithContext(ctx))

                        })

            }

}

 

type contextKey struct{}

 

var userCtxKey = contextKey{}

 

func TokenToContext(ctx context.Context, token *jwt.Token) context.Context {

   return context.WithValue(ctx, userCtxKey, token)

}

 

func TokenFromContext(ctx context.Context) (*jwt.Token, bool) {

   token, ok := ctx.Value(userCtxKey).(*jwt.Token)

   return token, ok

}

Такой подход:

  • делает middleware более гибким,
  • избавляет от глобальных переменных и хардкода,
  • соблюдает best practices работы с context.

P.S. Обратите внимание на тип ключа userCtxKey ;)

Проверка срока действия токена

В Go библиотека github.com/golang-jwt/jwt/v5 проверяет срок действия токена (exp) только, если вы явно используете jwt.RegisteredClaims. Это важно помнить при работе с произвольными структурами Claims.

Пример корректного объявления:

var claims jwt.RegisteredClaims

token, err := jwt.ParseWithClaims(tokenStr, &claims, keyFunc)

 

Даже если token.Valid == true, имеет смысл дополнительно проверить claims.ExpiresAt.Valid:

if claims.ExpiresAt != nil && claims.ExpiresAt.Before(time.Now()) {

    http.Error(w, "Token expired", http.StatusUnauthorized)

    return

}

Без этого exp может быть проигнорирован, особенно если вы определяете собственную структуру Claims, не встраивая RegisteredClaims.

Отдельный кейс – использование ParseUnverified

Если аутентификация выполняется внешним сервисом (например, Keycloak), то проверка подписи на вашем сервисе может быть необязательной. В таких случаях можно распарсить токен без валидации, извлекая нужные данные из payload.

var claims jwt.RegisteredClaims

parsedToken, _, err := new(jwt.Parser).ParseUnverified(tokenStr, &claims)

if err != nil {

    http.Error(w, "Invalid token format", http.StatusBadRequest)

    return

}

Этот подход полезен, когда:

  • Подпись уже проверена прокси, API-шлюзом или identity provider’ом.
  • Ваш код только читает содержимое токена (например, sub, email, roles), не доверяя ему как источнику авторизации.

Важно: ParseUnverified не проверяет подлинность токена. Используйте его только в окружениях, где подпись проверена заранее.

Заключение

Авторизация через JWT в Go – это не просто про «разобрал токен и проверил подпись». Это про понимание архитектуры, про надёжную валидацию, про контроль доступа без костылей и про чистый код, который можно отлаживать и масштабировать.

Вы узнали:

  • Как устроен jwt token, из чего он состоит и зачем нужен
  • Как правильно парсить токен с помощью github.com/golang-jwt/jwt
  • Какие ошибки (как следствие – уязвимости) допускают при валидации и как их избежать
  • как сделать authentication middleware

Но как вы понимаете, примеры выше достаточно примитивны и не имеют прямого отношения к настоящему боевому коду.

Познать же истинный хардкор, разобраться с пассивной и активной аутентификацией, работе с Keycloak, мидлварами в HTTP-фрейморке echo, а также авторизацией на базе JWT вы сможете на курсе "Искусство написания сервиса на Go".

Там используется JWT для авторизации клиентов и менеджеров в чате банковской поддержки, а также для подписи Kafka-сообщений между компонентами системы.

Удачи в коде – и пусть ваши токены всегда будут валидными.

Поделитесь своим опытом:

Комментарии проходят модерацию

0 комментариев