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

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

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

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

golang-jwt v5: первый токен

golang-jwt v5 — актуальная библиотека для jwt-токенов в Go, и создание jwt-токена в Go в ней умещается в пять строк: собрать, подписать, готово. В уроке поставим пакет, разберёмся, откуда он взялся и чем отличается от старого jwt-go, и соберём программу, которая печатает настоящий HS256-токен. Код пойдёт в локальный Go: песочница сайта внешние пакеты не импортирует.

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

У библиотеки JWT в Go непростая судьба, и её стоит знать, чтобы не скопировать в проект мёртвый пакет. Годы де-факто стандартом был dgrijalva/jwt-go — а к 2020 году он перестал поддерживаться, накопив в том числе CVE-2020-26160, обход проверки аудитории, закрытый уже в форке. Экосистема съехала на github.com/golang-jwt/jwt — поддерживаемый форк с тем же API, который с тех пор развивается самостоятельной жизнью. Сегодня это стандарт: пакет, на который переехали фреймворки, туториалы и большая часть продакшена. Пятая мажорная версия живёт в пути импорта как /v5 по правилам семантического версионирования Go. Отсюда практическое правило: встретили в старой статье импорт dgrijalva/jwt-go — читайте ради идей, а импорты и тонкости API переписывайте на v5. В пятой версии даты задаются через jwt.NewNumericDate, а ошибки парсинга агрегируются и различаются через errors.Is — об этом чуть ниже на живом примере.

Установка: три команды терминала

GO
mkdir auth && cd auth
go mod init example.com/auth
go get github.com/golang-jwt/jwt/v5

Стандартный старт любого библиотечного курса: модуль создаёт go mod init с именем проекта, зависимость подтягивает go get. В go.mod появится строка github.com/golang-jwt/jwt/v5 — и слэш /v5 обязателен в импорте: без него компилятор не найдёт пакет. Это семантическое версионирование Go, а не опечатка.

Что появилось в go.mod

GO
module example.com/auth

go 1.23

require github.com/golang-jwt/jwt/v5 v5.2.1

Файл go.mod после установки: require фиксирует конкретную версию — у вас она может быть новее, это нормально. Команда go mod tidy перепроверит зависимости и заполнит go.sum контрольными суммами. Мажор в пути импорта и в require должен совпадать: /v5 в обеих строках.

Создание jwt-токена в Go: NewWithClaims и SignedString#

В golang-jwt v5 выпуск токена распадается на две операции, и обе умещаются в одну строку каждая. jwt.NewWithClaims(method, claims) собирает заготовку: в неё уходят алгоритм подписи — для нашего курса jwt.SigningMethodHS256 — и набор claims, например jwt.MapClaims. Результат — указатель на jwt.Token, ещё не строка: токен не подписан. Вторая операция — SignedString(key): метод пересчитывает подпись тем алгоритмом, который был заявлен в NewWithClaims, и склеивает три части в финальную строку. Ключ для HS256 — срез байтов секрета, []byte(secret). Разделение не бюрократия: между сборкой и подписью можно заглянуть внутрь заготовки в отладчике, а сам интерфейс один и тот же для HS256, RS256 и ES256 — поменяются только алгоритм и тип ключа. Ошибка, если секрет окажется не того типа, вернётся второй возвращаемой величиной — привычная идиома Go, без паник и молчаливых провалов.

Первая программа: печатаем настоящий токен

GO
package main

import (
        "fmt"
        "os"
        "time"

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

func main() {
        secret := os.Getenv("JWT_SECRET")

        token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
                "sub":  "42",
                "role": "admin",
                "exp":  jwt.NewNumericDate(time.Now().Add(15 * time.Minute)),
                "iat":  jwt.NewNumericDate(time.Now()),
        })

        signed, err := token.SignedString([]byte(secret))
        if err != nil {
                fmt.Println("ошибка подписи:", err)
                return
        }

        fmt.Println(signed)
}

Программа целиком: секрет читается из переменной окружения JWT_SECRET, claims заданы jwt.MapClaims, даты — через jwt.NewNumericDate, потому что v5 работает с NumericDate, а не с голыми числами. exp на пятнадцать минут и iat на момент выпуска — минимум, без которого токен выпускать нельзя. Запуск: go run main.go после export JWT_SECRET=значение.

Что печатает программа: знакомые три части#

Запустите программу — и на экране появится строка, которую вы уже умеете читать после урока про устройство токена: два куска base64url, точка, ещё кусок. Первый декодируется в заголовок с alg HS256, второй — в ваши claims с sub и role, третий — подпись, непрозрачная строка байтов. Полезная привычка: прогнать только что выпущенный токен через jwt.io или любой оффлайн-декодер и убедиться, что содержимое совпадает с задуманным — так ловятся опечатки в именах полей до того, как они доедут до клиента. Заодно проверка наглядно показывает цену безопасности: декодер без вашего секрета пометит подпись невалидной и будет прав — он же не знает ключа. Сам токен на вид стандартен, и именно поэтому к нему легко привыкнуть как к чёрному ящику; знание анатомии из прошлого урока — то, что оставит ящик открытым.

Секрет и claims: из чего собирается токен#

Два ингредиента программы заслуживают отдельного слова. Секрет — строка из окружения, и её качество мы разберём в уроке про HMAC: пока важно лишь то, что она не хардкодится и не коммитится. Claims — это jwt.MapClaims, то есть map[string]any: имена полей произвольные, значения любые. Стандарту RFC 7519 соответствуют sub и exp; role — поле нашего проекта, и его права на жизнь обсуждаются в уроке про claims. Обратите внимание на exp: без него токен выпустится без единого предупреждения и будет валиден вечно — библиотека не заставляет срок годности при выпуске, это ваша ответственность как выпускающего. Пятнадцать минут — привычный срок для access-токена в паре с refresh-схемой, до которой курс доберётся в середине. А если хочется строгой гарантии на стороне проверки — она появится в виде опции WithExpirationRequired там же, в уроке про claims.

У самой простой программы урока есть тёмная сторона, и она стоит реальных инцидентов. Забыли ExpiresAt при выпуске, парсер на проверке остался без WithExpirationRequired — и токен, которого нет ни в базе, ни в сессиях, нельзя отозвать кнопкой, будет приниматься годами. Хуже того, такой токен часто выпускается в отладке с ролью admin и расползается по логам, скриншотам и тестовым записям. Утечка обычного токена лечится ожиданием: пятнадцать минут — и мёртв. Утечка вечного лечится только ротацией секрета, то есть перелогином всех пользователей сразу. Диагностика простая: декодируйте payload подозрительного токена — если exp в нём нет, перед вами не баг проверки, а дыра выпуска. Страховка умещается в две строки: exp при каждом выпуске и WithExpirationRequired при каждой проверке; обе разбираем в следующем уроке про claims.

Токен без проверки — половина дела, и библиотека позволяет проверить выпущенное в той же программе. jwt.ParseWithClaims принимает строку, контейнер для claims и keyfunc — функцию, которая возвращает ключ проверки. Для HS256 это тот же []byte(secret). Опция jwt.WithValidMethods([]string{"HS256"}) фиксирует ожидаемый алгоритм и отбрасывает токены с чужим alg до всякой проверки подписи — привычка, которую стоит ставить с первого дня. Если ошибок нет и parsed.Valid равен true, токен подписан нами и не просрочен, а claims уже декодированы в переданный контейнер. Полный разбор парсинга, keyfunc и всех типов ошибок — тема шестого урока, здесь важен сам факт: выпуск и проверка живут в одной библиотеке и делят один секрет.

Проверка тем же секретом

GO
parsed, err := jwt.ParseWithClaims(signed, &jwt.MapClaims{},
        func(t *jwt.Token) (any, error) {
                return []byte(secret), nil
        },
        jwt.WithValidMethods([]string{"HS256"}))

if err != nil || !parsed.Valid {
        fmt.Println("токен отвергнут:", err)
        return
}

claims := *parsed.Claims.(*jwt.MapClaims)
fmt.Println(claims["sub"], claims["role"])

ParseWithClaims режет строку, декодирует header и payload, достаёт ключ из keyfunc и сверяет подпись. WithValidMethods отсекает чужие алгоритмы заранее. Парная проверка err и parsed.Valid — идиома v5: ошибки агрегированы, а claims к моменту успеха уже лежат в MapClaims и читаются по именам.

Спорное мнение: JWT — не замена сессиям#

Урок был бы неполным без спорного мнения: JWT — не автоматическое улучшение классических серверных сессий. Сессия в Redis отзывается мгновенно: сервер удалил ключ — и всё. Токен живёт у клиента до exp, и мгновенный выход требует denylist по jti, то есть хранилища и похода в него при каждой проверке — ровно того stateless, ради которого JWT обычно брали. Если у вас монолит с серверными страницами и один процесс, cookie-сессия проще и честнее. JWT выигрывает там, где проверять токены должны несколько независимых сервисов без общего хранилища: шлюз сверяет подпись на входе, бэкенд и фоновые воркеры делают то же, и никто не ходит в базу сессий. Я выбираю JWT осознанно, под задачу распределённой проверки, а не по умолчанию — правильный вопрос на ревью не «JWT или сессии», а «кто и где проверяет». Цене отзыва посвящён урок про denylist.

Чек-лист первого токена#

  • Модуль создан go mod init, пакет подтянут go get github.com/golang-jwt/jwt/v5.
  • Импорт — github.com/golang-jwt/jwt/v5, со слэшем /v5.
  • Секрет приходит из переменной окружения, а не из исходников.
  • У claims заполнены exp и iat через jwt.NewNumericDate.
  • Алгоритм заявлен константой jwt.SigningMethodHS256, а не строкой из header.
  • Проверка при парсинге фиксирует ожидаемый алгоритм через jwt.WithValidMethods.

Проверка: путь импорта

Какой путь импорта у пятой версии библиотеки?

Проверка: SignedString

Что делает метод SignedString у jwt.Token?

Токен без exp: сколько частей?

Прочитайте код и выберите, что он выведет:

signed, _ := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        "sub": "42",
}).SignedString([]byte("secret"))

fmt.Println(len(strings.Split(signed, ".")))

Выпуск токена: два вызова

Допишите сборку заготовки и подпись.

token := jwt.(jwt.SigningMethodHS256, jwt.MapClaims{ "sub": "42", "exp": jwt.NewNumericDate(time.Now().Add(15 * time.Minute)), }) signed, err := token.([]byte(secret))

Соберите main() выпускного однострочника

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

Строки

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

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

Карточки: golang-jwt v5

Термин

golang-jwt/jwt/v5

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

Поддерживаемый форк замороженного dgrijalva/jwt-go, стандарт экосистемы. Слэш /v5 в пути импорта обязателен.

1 / 6

Первый токен выпущен и проверен, дальше — содержимое. В уроке про claims разберём стандартные поля RFC 7519, заведём собственную структуру claims и настроим автоматическую проверку дат, а следом в уроке про HMAC — наконец поймём, что именно делает SignedString с секретом внутри. Если HTTP-фреймворка в проекте ещё нет, самое время выбрать: middleware из середины курса мы напишем и на чистом net/http, и на chi с Gin. Код этого урока запускается локальным Go — песочница сайта не импортирует внешние пакеты, поэтому установка с go.dev остаётся обязательным шагом домашнего задания.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

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

  • dgrijalva/jwt-go заморожен с CVE; актуальная библиотека — github.com/golang-jwt/jwt/v5.
  • Выпуск токена — две операции: jwt.NewWithClaims собирает заготовку, SignedString подписывает и склеивает строку.
  • Секрет HS256 — []byte из переменной окружения; в исходниках ему не место.
  • Даты в v5 задаются jwt.NewNumericDate; exp и iat обязательны для каждого выпускаемого токена.
  • Проверка в один заход — ParseWithClaims c WithValidMethods, успех по parsed.Valid.
  • Код курса требует локального Go: песочница не импортирует внешние пакеты.

Домашнее задание

Пройдите путь урока локально: go mod init, go get github.com/golang-jwt/jwt/v5, программа из урока с секретом из окружения. Выпустите токен, вставьте его в декодер и сверьте header и payload с задуманными. Затем выпустите второй токен с role user и сравните их payload глазами: различие должно быть ровно в одном поле.

Личные заметкиПусто
Хранятся только в вашем браузере (JWT · golang-jwt v5: первый токен)

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

Чем golang-jwt отличается от jwt-go?

golang-jwt — форк замороженного dgrijalva/jwt-go, который развивается сообществом: закрытые CVE, NumericDate для дат, структурированные ошибки в v5. API близкий, импорт другой — туториалы по старому пакету читают ради идей, а не копируют.

Каким должен быть секрет?

Для HS256 секрет — просто срез байтов, но не строка из словаря: 32 байта и больше, сгенерированные криптографическим генератором. Сложность важна не форме, а энтропии: перебор длинных случайных ключей неокупаем.

Почему токен не выпустить в песочнице сайта?

Песочница интерпретирует подмножество стандартной библиотеки и не умеет внешние пакеты, сеть и файловую систему. Библиотечные курсы проекта рассчитаны на локальный Go: код урока запускается go run после установки с go.dev.

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

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

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

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

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

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

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