Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 6 из 25 ~18 мин +20 XP

Проверка токена: ParseWithClaims и Keyfunc

Токен, подписанный в [прошлом уроке про HMAC-подпись](/courses/jwt-go/lessons/05-hmac-podpis/), прилетел в запросе — серверу нужна проверка jwt токена, прежде чем отвечать данными. Разбираем jwt.ParseWithClaims и keyfunc в golang-jwt v5: где живёт сверка подписи, что парсер проверяет сам и почему вся политика доверия умещается в функцию из пяти строк — и именно в ней чаще всего оставляют дыру.

Содержание урока · 7 разделов

После выпуска токена в уроке про 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 заполнена. Третьего состояния нет, и это упрощает жизнь: проверка всегда двоична.

Функция проверки целиком

GO
package 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 — вовсе без подписи. Уязвимость давно закрыта, но приём «подменить алгоритм» жив в другой форме, и о ней — врезка ниже.

Достаём токен из запроса

GO
func 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

GO
claims := &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.

Восстановите вызов проверки

Допишите вызов ParseWithClaims, утверждение с ok и возврат ключа из keyfunc.

claims := &AppClaims{} _, err := jwt.(tokenStr, claims, func(t *jwt.Token) (any, error) { if _, ok := t.Method.(*jwt.SigningMethodHS256); ! { return nil, fmt.Errorf("неожиданный alg") } []byte(os.Getenv("JWT_SECRET")), nil })

Соберите keyfunc построчно

Расставьте строки keyfunc в правильном порядке: от сигнатуры до возврата секрета.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: проверка токена

Термин

jwt.ParseWithClaims

нажмите, чтобы перевернуть
Определение

Точка проверки в golang-jwt v5: режет строку, сверяет подпись через keyfunc, проверяет exp, iat и nbf, заполняет вашу структуру claims. Ошибка nil — токен годен.

1 / 5

Следующий шаг — понять, что делать с ошибками разбора: клиенту с истёкшим 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 · Проверка токена: ParseWithClaims и Keyfunc)

Вопросы и ответы по уроку

Чем 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, и подмену семейства алгоритмов.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Telegram25 уроков~12 чновичок25 уроков уже доступны

Telegram-боты на Go: от эха до полноценного сервиса

Учимся писать Telegram-ботов на Go: токен BotFather, команды, клавиатуры, состояния диалога, медиа, рассылки, webhook и деплой. 25 уроков с кодом.

  • Создавать бота через BotFather и хранить токен безопасно
  • Получать обновления через long polling и webhook
  • Отвечать сообщениями, командами и клавиатурами
  • Строить многошаговые диалоги с сохранением состояния