Введение
Работа с 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
Токен состоит из трёх частей, разделённых точками:
- Header – указывает тип токена и алгоритм подписи (чаще всего RSA или на худой конец HMAC).
- Payload – содержит claims: идентификаторы, роли, срок действия и прочие данные.
- 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 с динамической обработкой алгоритма:
- Сервер генерирует токены с alg: RS256 (RSA) и проверяет их с помощью публичного ключа.
- Клиент (или злоумышленник) формирует свой токен, подписывая его утекшим тем же публичным ключом, но меняет алгоритм на HS256. ({“alg”: “HS256”, “typ”: “JWT”}).
- Если сервер доверяет 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-сообщений между компонентами системы.
Удачи в коде – и пусть ваши токены всегда будут валидными.
Понравился пост, ставь лайк
Поделитесь своим опытом:
Комментарии проходят модерацию