После выпуска токена в уроке про HMAC у сервера появляется обратная задача: по строке из запроса восстановить claims и решить, подписывал ли их действительно наш сервис. Наивный путь — раскодировать base64 руками, достать payload и поверить ему. Так делают в половине самописных авторизаций, и это ровно то место, где проект начинает пускать кого угодно: base64 декодируется кем угодно, а подпись никто не сверял. Разбор jwt-токена в Go обязан идти через библиотеку, которая делает всё сразу: декодирует части, проверяет подпись, сверяет exp, iat и nbf и только потом отдаёт claims. Библиотека — golang-jwt/jwt v5, тот же пакет, которым мы выпускали токены, для проверки он не приносит новых зависимостей. Запросы вида jwt parse go в поисковиках почти всегда про одну и ту же пару: jwt.ParseWithClaims и keyfunc. Ниже полный разбор: что происходит под капотом, за что отвечает keyfunc, что библиотека проверяет сама и какие две ошибки в keyfunc превращают защищённый API в открытую дверь.
ParseWithClaims: анатомия проверки jwt токена#
Сигнатура: jwt.ParseWithClaims(tokenString, &claims, keyFunc, options...) — возвращает пару (*jwt.Token, error) и заодно заполняет структуру claims. Полезно держать в голове пять шагов, из которых состоит проверка, — по ним потом читаются ошибки. Первый шаг: разбор строки на три части, разделённые точками; частей не ровно три или одна пустая — всё заканчивается ошибкой malformed ещё до криптографии. Второй шаг: декодирование header и payload из base64url в JSON и раскладка payload по вашей структуре claims — в тот же тип, что вы использовали при выпуске. Третий шаг: вызов keyfunc — библиотека не знает, каким ключом вы подписывали, поэтому спрашивает у вас; полученным ключом она сверяет подпись первых двух частей. Четвёртый шаг: проверка времени — exp, iat и nbf из jwt.RegisteredClaims сверяются с текущим временем автоматически; поле не заполнено — проверка по нему пропускается. Пятый шаг — ваш: бизнес-контроль issuer, audience и ролей. Хоть один шаг упал — err не nil и верить токену нельзя; err == nil значит токен жив, подписан и структура claims заполнена. Третьего состояния нет, и это упрощает жизнь: проверка всегда двоична.
Функция проверки целиком
GOpackage auth
import (
"fmt"
"os"
"github.com/golang-jwt/jwt/v5"
)
type AppClaims struct {
UserID int64
Role string
jwt.RegisteredClaims
}
var verifyKey = func(t *jwt.Token) (any, error) {
if _, ok := t.Method.(*jwt.SigningMethodHS256); !ok {
return nil, fmt.Errorf("неожиданный alg: %v", t.Header["alg"])
}
return []byte(os.Getenv("JWT_SECRET")), nil
}
func VerifyToken(tokenStr string) (*AppClaims, error) {
claims := &AppClaims{}
_, err := jwt.ParseWithClaims(tokenStr, claims, verifyKey)
if err != nil {
return nil, err
}
return claims, nil
}AppClaims — тот же тип, что при выпуске: UserID и Role плюс встроенные jwt.RegisteredClaims со сроками. Keyfunc допускает ровно один алгоритм и возвращает секрет тем же []byte, каким подписывали в уроке про HMAC, — расхождение типа ключа и метода даёт отказ. Ошибка возвращается вызывающему как есть: различать её причины научимся в следующем уроке.
Keyfunc: единственное место доверия#
Зачем библиотеке ваша функция, если алгоритм и так написан в header токена? Именно поэтому и нужна: содержимое header пишет сам клиент, и верить строке alg из токена — всё равно что верить надписи «паспорт настоящий» на паспорте. Keyfunc — точка, где сервер говорит: я подписываю HS256 вот этим секретом, всё остальное мусор. Здесь живут два обязательных замка. Первый — проверка метода: утверждение t.Method.(*jwt.SigningMethodHS256) убеждается, что токен подписан тем алгоритмом, который вы реально используете; токен с другим alg отвергается до сверки подписи. Второй — ключ: для HS256 это тот же []byte-секрет, что при подписи; вернуть его строкой нельзя, метод HMAC ожидает байты и ответит ошибкой неподходящего типа ключа. Историческая справка, зачем замок первый: старые версии некоторых JWT-библиотек принимали токены с alg none — вовсе без подписи. Уязвимость давно закрыта, но приём «подменить алгоритм» жив в другой форме, и о ней — врезка ниже.
Самая частая дыра при разборе jwt токена — keyfunc без проверки метода: return secret, nil на любой alg. У приёма есть имя — алгоритмическая путаница: сервер подписывает RS256 приватным ключом, наивный keyfunc отдаёт его же байты как HMAC-секрет, и атакующий меняет alg на HS256, подписывая токен публичным ключом, который и так лежит на JWKS-странице. Секрет ему не нужен — он публичный. Моё правило: проверка метода обязательна всегда. Допускаю один отказ — когда рядом стоит jwt.WithValidMethods, который режет допустимые алгоритмы на уровне парсера: тогда даже дырявый keyfunc не пропустит чужое семейство.
Достаём токен из запроса
GOfunc currentUser(r *http.Request) (*AppClaims, error) {
header := r.Header.Get("Authorization")
const prefix = "Bearer "
if !strings.HasPrefix(header, prefix) {
return nil, errors.New("нет схемы Bearer в Authorization")
}
return VerifyToken(strings.TrimPrefix(header, prefix))
}Транспорт токена — заголовок Authorization со схемой Bearer и ровно одним пробелом. HasPrefix отсеивает пустые заголовки и чужие схемы, TrimPrefix срезает схему, оставляя саму строку токена. Функция — уже слой транспорта: VerifyToken ничего не знает про HTTP и переиспользуется где угодно, а цельный middleware с этой же логикой соберём в десятом уроке курса.
Что проверяет библиотека, а что остаётся вам#
Граница ответственности — источник недопонимания, поэтому проговорим её отдельно. Библиотека отвечает за криптографию и календарь: подпись по ключу из keyfunc, exp, iat и nbf из RegisteredClaims; истёкший exp вернёт jwt.ErrTokenExpired, и как отличать причины отказов — тема следующего урока. Библиотека не знает ваших бизнес-правил, поэтому за вами три вещи. Смысл полей: UserID из payload — утверждение, которое сервер сам положил при выпуске, и верить ему можно ровно настолько, насколько верна подпись. Issuer и audience: если токены выпускают несколько сервисов, включайте проверку опциями jwt.WithIssuer и jwt.WithAudience — обе передаются четвёртым и далее аргументами ParseWithClaims. Свежесть решения: stateless-проверка не видит состояния, отозванный токен она не заметит — если отзыв нужен, между разбором и ответом встанет своя проверка списка. Парсер настраивается и по времени: jwt.WithLeeway прощает небольшую рассинхронизацию часов между машинами, jwt.WithTimeFunc подменяет точку отсчёта в тестах.
Кто за что отвечает при проверке#
- Формат строки — библиотека: не три части, битый base64url или невалидный JSON дают отказ ещё до криптографии.
- Подпись и алгоритм — библиотека плюс ваш keyfunc: сверка HMAC по секрету и отсечение чужих alg.
- exp, iat, nbf — библиотека автоматически, если claims построены на jwt.RegisteredClaims.
- iss и aud — библиотека по опциям jwt.WithIssuer и jwt.WithAudience, когда несколько выпускающих.
- Роли и пользователь — вы: поля claims читает код хендлера после успешного разбора.
- Отзыв токена — вы: stateless-проверка не знает про denylist, это отдельная задача с хранилищем.
Парсер с опциями: страховка от подмены alg
GOclaims := &AppClaims{}
_, err := jwt.ParseWithClaims(tokenStr, claims, verifyKey,
jwt.WithValidMethods([]string{"HS256"}),
jwt.WithIssuer("gomaster"),
jwt.WithLeeway(30*time.Second),
)WithValidMethods перечисляет единственный допустимый алгоритм и отсекает подмену alg ещё до вызова keyfunc — страховка, независимая от аккуратности вашей функции. WithIssuer требует совпадения iss, WithLeeway даёт полминуты на разъехавшиеся часы сервера и машины, выпустившей токен. Опции не заменяют keyfunc: ключ всё равно спрашивают у вас.
Проверка: успешный разбор
jwt.ParseWithClaims вернул err == nil. Что вы обязаны сделать перед тем, как читать claims?
Проверка: тип ключа в keyfunc
Какой тип должна возвращать keyfunc для токена, подписанного HS256?
Чужой алгоритм на входе
Прочитайте код и выберите, что он выведет:
// keyfunc пропускает только *jwt.SigningMethodHS256
tokenStr := "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxIn0.c2lnbmF0dXJl"
claims := &AppClaims{}
_, err := jwt.ParseWithClaims(tokenStr, claims, verifyKey)
// что в err?Практический вопрос, на котором спотыкаются: токен уже долетел до сервера — где его взять и когда проверять. Каноничное место — заголовок Authorization со схемой Bearer, как в currentUser выше; каноничный момент — как можно раньше, до хендлеров, чтобы защищённые маршруты вообще не помнили про токены. Для этого проверка заворачивается в middleware: в chi это функция с сигнатурой func(next http.Handler) http.Handler, и в уроке про middleware аутентификации мы соберём её из сегодняшних деталей — как и из уроков курса по роутеру chi, где эта сигнатура — основа всего. Разнесение по слоям помогает и тестам: VerifyToken принимает строку и возвращает claims — её проверяют юнит-тестом без HTTP. Честная оговорка про запуск: это библиотечный курс, и в браузерной песочнице сайта такой код не поднимется — песочница не умеет ни сетевых сокетов, ни внешних модулей; для живой проверки нужен локальный Go, установка пакета и пара curl-запросов к вашему серверу. Остальное в уроке работает и без запуска: тесты и разборы построены на реальном поведении пакета v5.
Нужны локальный Go и три команды: go mod init, go get github.com/golang-jwt/jwt/v5, go run. Секрет положите в переменную окружения JWT_SECRET и экспортируйте перед запуском, чтобы выпуск из пятого урока и проверка из этого читали одно значение: расхождение секретов даёт отказ подписи, а не понятную ошибку.
Восстановите вызов проверки
Допишите вызов ParseWithClaims, утверждение с ok и возврат ключа из keyfunc.
Соберите keyfunc построчно
Расставьте строки keyfunc в правильном порядке: от сигнатуры до возврата секрета.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: проверка токена
Следующий шаг — понять, что делать с ошибками разбора: клиенту с истёкшим exp нужна одна подсказка, а клиенту с подделанной подписью — совсем другая. В уроке про ошибки токенов разложим jwt.ErrTokenExpired, jwt.ErrTokenMalformed и остальных через errors.Is и соберём честные ответы API. Если вы пришли в курс с gin, тот же парсер в его терминах — c.Set, AbortWithStatusJSON, группы маршрутов — разобран в уроке про JWT в gin: механика общая, отличается транспорт. Вернуться к карте курса и перечитать выпуск токена, claims и HMAC-подпись всегда можно на странице курса jwt-go — без этих уроков сегодняшняя проверка не собирается.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Проверка jwt токена идёт через jwt.ParseWithClaims: разбор строки, сверка подписи через keyfunc, автоматические exp, iat и nbf.
- Keyfunc — единственное место доверия: проверка t.Method плюс возврат ключа нужного типа, для HS256 это []byte.
- err == nil значит годный токен и заполненные claims; token.Valid дублирует отсутствие ошибки.
- jwt.WithValidMethods, jwt.WithIssuer и jwt.WithLeeway дополняют keyfunc: белый список alg, проверка iss, допуск по часам.
- Из запроса токен достаётся из Authorization: Bearer и проверяется до хендлеров — в middleware десятого урока.
- Запуск кода — локальным Go: песочница сайта не умеет сокеты и внешние модули.
Домашнее задание
Соберите VerifyToken локально: go get github.com/golang-jwt/jwt/v5, секрет из JWT_SECRET. Выпустите токен функцией из урока про HMAC-подпись и прогоните через VerifyToken — claims должны вернуться. Затем выпустите токен с exp в прошлом и посмотрите на ошибку разбора: её текст понадобится в следующем уроке про ошибки.
Личные заметкиПусто
Вопросы и ответы по уроку
Чем jwt.Parse отличается от jwt.ParseWithClaims?
Parse кладёт payload в jwt.MapClaims — карту с типами any, где числа становятся float64. ParseWithClaims декодирует payload в вашу структуру: типы сразу правильные, опечатка в поле ловится компилятором, а не в рантайме. Для продакшена удобнее второй вариант.
Нужно ли проверять token.Valid после успешного ParseWithClaims?
В v5 отсутствие ошибки гарантирует валидность: подпись, алгоритм и сроки уже проверены, token.Valid равен true автоматически. Отдельная проверка не вредна, но необходимой её назвать нельзя — она дублирует сравнение err с nil.
Почему нельзя доверять alg из header токена?
Header пишет сам клиент, поэтому alg — утверждение нападающего, а не факт. Защита двусторонняя: проверка t.Method в keyfunc плюс jwt.WithValidMethods на парсере. Вместе они исключают и исторический alg none, и подмену семейства алгоритмов.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Telegram-боты на Go: от эха до полноценного сервиса
Учимся писать Telegram-ботов на Go: токен BotFather, команды, клавиатуры, состояния диалога, медиа, рассылки, webhook и деплой. 25 уроков с кодом.
- Создавать бота через BotFather и хранить токен безопасно
- Получать обновления через long polling и webhook
- Отвечать сообщениями, командами и клавиатурами
- Строить многошаговые диалоги с сохранением состояния