---
title: "Проверка токена: ParseWithClaims и Keyfunc"
description: "Проверка jwt токена на Go: ParseWithClaims и keyfunc в golang-jwt v5 — сверка подписи, авто-проверка exp, WithValidMethods и ошибки разбора. С примерами."
course: "JWT-аутентификация в Go: golang-jwt и bcrypt"
url: "https://golangmaster.ru/courses/jwt-go/lessons/06-parsing-token/"
published: "2026-09-19"
updated: "2026-10-02"
minutes: 18
---

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

*Курс «JWT-аутентификация в Go: golang-jwt и bcrypt», урок 6 из 25. ~18 минут. Обновлено 02.10.2026. Источник: https://golangmaster.ru/courses/jwt-go/lessons/06-parsing-token/*

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

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

> **Живое место: keyfunc, которая просто возвращает секрет.** Самая частая дыра при разборе jwt токена — keyfunc без проверки метода: return secret, nil на любой alg. У приёма есть имя — алгоритмическая путаница: сервер подписывает RS256 приватным ключом, наивный keyfunc отдаёт его же байты как HMAC-секрет, и атакующий меняет alg на HS256, подписывая токен публичным ключом, который и так лежит на JWKS-странице. Секрет ему не нужен — он публичный. Моё правило: проверка метода обязательна всегда. Допускаю один отказ — когда рядом стоит jwt.WithValidMethods, который режет допустимые алгоритмы на уровне парсера: тогда даже дырявый keyfunc не пропустит чужое семейство.

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

```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?

- ✅ Ничего: подпись верна, сроки не вышли, claims заполнены
- Проверить token.Valid вручную — парсер его не заполняет
- Ещё раз сверить exp своими руками — библиотека его не смотрит
- Переподписать токен сервером перед использованием

В golang-jwt v5 отсутствие ошибки гарантирует, что все шаги прошли: разбор строки, сверка подписи, проверка сроков. token.Valid парсер устанавливает сам, exp для RegisteredClaims проверяется автоматически, а переподписывать токен на стороне проверки не нужно и бессмысленно — клиент всё равно получил бы ту же строку.

### Проверка: тип ключа в keyfunc

**Вопрос:** Какой тип должна возвращать keyfunc для токена, подписанного HS256?

- string с тем же секретом
- ✅ []byte с тем же секретом, что подписывал токен
- Структуру jwt.SigningKey с алгоритмом и ключом
- Любой тип: метод приведёт ключ сам

Метод HMAC в golang-jwt ожидает ключ как []byte: строка того же содержимого вернёт ошибку несоответствия типа ключа, и токен будет отвергнут, хотя секреты совпадают. Тип ключа зависит от семейства: для RSA это структуры *rsa.PrivateKey и *rsa.PublicKey, для ECDSA — *ecdsa.PrivateKey и *ecdsa.PublicKey.

### Чужой алгоритм на входе

```go
// keyfunc пропускает только *jwt.SigningMethodHS256
tokenStr := "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxIn0.c2lnbmF0dXJl"

claims := &AppClaims{}
_, err := jwt.ParseWithClaims(tokenStr, claims, verifyKey)
// что в err?
```

**Что выведет программа?**

- nil: keyfunc проверяет метод, но не содержимое подписи
- ✅ Ошибка от keyfunc: alg RS256 не проходит утверждение про HS256, до сверки подписи дело не доходит
- Подпись сойдёт: HS256 и RS256 считают одну функцию
- Паника внутри библиотеки на незнакомом алгоритме

HS256 и RS256 — разные семейства: HMAC с общим секретом и RSA с парой ключей, их подписи несовместимы в принципе. Утверждение t.Method.(*jwt.SigningMethodHS256) не выполнится, keyfunc вернёт ошибку, и ParseWithClaims завершится ею — сверять подпись библиотека даже не начнёт. Паник в golang-jwt на чужих алгоритмах нет: любой отказ идёт через error.

Практический вопрос, на котором спотыкаются: токен уже долетел до сервера — где его взять и когда проверять. Каноничное место — заголовок Authorization со схемой Bearer, как в currentUser выше; каноничный момент — как можно раньше, до хендлеров, чтобы защищённые маршруты вообще не помнили про токены. Для этого проверка заворачивается в middleware: в chi это функция с сигнатурой func(next http.Handler) http.Handler, и в [уроке про middleware аутентификации](/courses/jwt-go/lessons/10-middleware-auth/) мы соберём её из сегодняшних деталей — как и из уроков [курса по роутеру chi](/courses/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.

```go
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
})
```

Подсказка: Разбор выполняет ParseWithClaims, форма присваивания с проверкой даёт ok, а keyfunc обязана вернуть ключ и ошибку.

Правильные ответы в пропусках: `ParseWithClaims`, `ok`, `return`

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

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

Правильный порядок строк:

1. `var verifyKey = func(t *jwt.Token) (any, error) {`
2. `if _, ok := t.Method.(*jwt.SigningMethodHS256); !ok {`
3. `return nil, fmt.Errorf("неожиданный alg: %v", t.Header["alg"])`
4. `} // не HS256 — отказ`
5. `return []byte(os.Getenv("JWT_SECRET")), nil`
6. `}`

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

- **jwt.ParseWithClaims** — Точка проверки в golang-jwt v5: режет строку, сверяет подпись через keyfunc, проверяет exp, iat и nbf, заполняет вашу структуру claims. Ошибка nil — токен годен.
- **Keyfunc** — Функция func(t *jwt.Token) (any, error), которой парсер спрашивает ключ проверки. Обязательна проверка t.Method: алгоритм из header токена доверия не заслуживает.
- **token.Valid** — Флаг токена после разбора: true тогда и только тогда, когда err == nil. Дублирует отсутствие ошибки, отдельной проверки не требует.
- **jwt.WithValidMethods** — Опция парсера: белый список алгоритмов, например jwt.WithValidMethods([]string{"HS256"}). Отсекает подмену alg до вызова keyfunc.
- **jwt.WithLeeway** — Опция парсера: допуск по времени для exp, iat и nbf — на разъехавшиеся часы между машинами. В тестах точку отсчёта меняет jwt.WithTimeFunc.

Следующий шаг — понять, что делать с ошибками разбора: клиенту с истёкшим exp нужна одна подсказка, а клиенту с подделанной подписью — совсем другая. В уроке про [ошибки токенов](/courses/jwt-go/lessons/07-oshibki-tokenov/) разложим jwt.ErrTokenExpired, jwt.ErrTokenMalformed и остальных через errors.Is и соберём честные ответы API. Если вы пришли в курс с gin, тот же парсер в его терминах — c.Set, AbortWithStatusJSON, группы маршрутов — разобран в [уроке про JWT в gin](/courses/gin/lessons/15-jwt/): механика общая, отличается транспорт. Вернуться к карте курса и перечитать [выпуск токена](/courses/jwt-go/lessons/03-ustanovka-golang-jwt/), [claims](/courses/jwt-go/lessons/04-claims/) и HMAC-подпись всегда можно [на странице курса jwt-go](/courses/jwt-go/) — без этих уроков сегодняшняя проверка не собирается.

## Коротко: что запомнить

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