golang-jwt для Go: полное руководство с примерами
Библиотека golang-jwt (официальное название — github.com/golang-jwt/jwt/v5) — это де-факто стандартная реализация JSON Web Tokens (JWT) для языка Go. Она предоставляет разработчикам мощный и гибкий инструментарий для создания, подписи, верификации и декодирования JWT-токенов. JWT — это компактный и самодостаточный способ передачи информации между сторонами в виде JSON-объекта, который может быть верифицирован, так как он подписан цифровой подписью. Токены JWT широко используются для аутентификации, авторизации и безопасного обмена данными в REST API, микросервисных архитектурах и веб-приложениях.
Зачем нужна эта библиотека? В современной веб-разработке на Go практически любое приложение, требующее аутентификации пользователей (например, API-серверы, микросервисы, системы единого входа), сталкивается с необходимостью работы с JWT. Библиотека golang-jwt берёт на себя всю сложность: генерацию и проверку подписей (HMAC, RSA, ECDSA, EdDSA), парсинг заголовков и полезной нагрузки, обработку стандартных полей (exp, iat, iss, sub) и кастомных claims. Без неё пришлось бы вручную реализовывать криптографические операции и парсинг JSON, что чревато ошибками и уязвимостями.
Ключевые преимущества golang-jwt: полная поддержка спецификации RFC 7519, простота использования благодаря fluent-интерфейсу, высокая производительность (использует стандартную библиотеку crypto), возможность расширения через кастомные signing methods и валидаторы. Библиотека активно поддерживается сообществом, имеет более 7 тысяч звёзд на GitHub и используется в таких проектах, как Kubernetes, Docker, Grafana и многие другие.
Установка
Установка библиотеки производится стандартным для Go способом через go get. Убедитесь, что у вас установлен Go версии 1.18 или выше (рекомендуется 1.21+).
go get github.com/golang-jwt/jwt/v5
После выполнения команды в вашем go.mod появится зависимость. Импортировать библиотеку в код нужно следующим образом:
import "github.com/golang-jwt/jwt/v5"
Быстрый старт — минимальный рабочий пример
Создадим простой пример: генерация и верификация JWT-токена с использованием симметричного ключа (HMAC-SHA256).
package main
import (
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
)
func main() {
// 1. Секретный ключ (должен храниться в безопасности)
secretKey := []byte("my-super-secret-key-12345")
// 2. Создание токена
claims := jwt.MapClaims{
"user_id": 12345,
"role": "admin",
"exp": time.Now().Add(time.Hour * 24).Unix(), // срок действия 24 часа
"iat": time.Now().Unix(), // время создания
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(secretKey)
if err != nil {
panic(err)
}
fmt.Println("Сгенерированный токен:", tokenString)
// 3. Верификация и парсинг токена
parsedToken, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Проверяем, что метод подписи — HMAC
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("неожиданный метод подписи: %v", token.Header["alg"])
}
return secretKey, nil
})
if err != nil {
panic(err)
}
// 4. Извлечение claims
if claims, ok := parsedToken.Claims.(jwt.MapClaims); ok && parsedToken.Valid {
fmt.Printf("Пользователь ID: %v\n", claims["user_id"])
fmt.Printf("Роль: %v\n", claims["role"])
} else {
fmt.Println("Токен невалиден")
}
}
Основные методы и функции
Рассмотрим ключевые методы библиотеки. Для каждого приведена сигнатура, описание и рабочий пример.
1. jwt.NewWithClaims
Сигнатура: func NewWithClaims(method SigningMethod, claims Claims) *Token
Описание: Создаёт новый JWT-токен с указанным методом подписи и набором claims (утверждений). Возвращает указатель на структуру Token, который затем можно подписать.
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": "user123",
"exp": time.Now().Add(time.Hour).Unix(),
})
2. token.SignedString
Сигнатура: func (t *Token) SignedString(key interface{}) (string, error)
Описание: Подписывает токен с помощью предоставленного ключа и возвращает готовую JWT-строку. Тип ключа зависит от метода подписи (для HMAC — []byte, для RSA — *rsa.PrivateKey).
tokenString, err := token.SignedString([]byte("secret-key"))
3. jwt.Parse
Сигнатура: func Parse(tokenString string, keyFunc Keyfunc) (*Token, error)
Описание: Парсит и верифицирует JWT-строку. Принимает функцию Keyfunc, которая должна вернуть ключ для проверки подписи. Возвращает распарсенный токен или ошибку.
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return []byte("secret-key"), nil
})
4. jwt.ParseWithClaims
Сигнатура: func ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error)
Описание: Аналог Parse, но позволяет передать конкретную структуру claims для автоматического заполнения. Удобно для работы с кастомными типами.
type CustomClaims struct {
jwt.RegisteredClaims
UserID int `json:"user_id"`
}
claims := &CustomClaims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
return []byte("secret-key"), nil
})
5. jwt.MapClaims
Сигнатура: type MapClaims map[string]interface{} (реализует интерфейс Claims)
Описание: Простейший тип claims, представляющий собой обычную map. Подходит для быстрых прототипов и случаев, когда структура claims динамическая.
claims := jwt.MapClaims{
"username": "john_doe",
"admin": true,
"exp": time.Now().Add(15 * time.Minute).Unix(),
}
6. jwt.RegisteredClaims
Сигнатура: type RegisteredClaims struct { ... }
Описание: Структура со стандартными полями JWT (iss, sub, aud, exp, nbf, iat, jti). Рекомендуется встраивать в кастомные структуры claims для соблюдения спецификации.
type MyClaims struct {
jwt.RegisteredClaims
Scope string `json:"scope"`
}
claims := MyClaims{
RegisteredClaims: jwt.RegisteredClaims{
Subject: "user123",
ExpiresAt: jwt.NewNumericDate(time.Now().Add(30 * time.Minute)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
Scope: "read:users",
}
7. jwt.NewNumericDate
Сигнатура: func NewNumericDate(t time.Time) *NumericDate
Описание: Создаёт объект NumericDate из time.Time. Используется для установки полей ExpiresAt, IssuedAt и NotBefore в RegisteredClaims.
expTime := jwt.NewNumericDate(time.Now().Add(24 * time.Hour))
8. jwt.SigningMethodHMAC
Сигнатура: type SigningMethodHMAC struct { ... }
Описание: Представляет метод подписи HMAC (HS256, HS384, HS512). Используется для проверки алгоритма подписи при верификации токена.
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("неверный метод подписи: %v", token.Header["alg"])
}
return []byte("secret"), nil
})
9. jwt.ValidationError
Сигнатура: type ValidationError struct { ... }
Описание: Тип ошибки, возвращаемой при невалидности токена. Содержит флаги для определения причины: истек срок, неверная подпись, неправильный метод и т.д.
if err != nil {
if ve, ok := err.(*jwt.ValidationError); ok {
if ve.Errors&jwt.ValidationErrorExpired != 0 {
fmt.Println("Токен истек")
}
}
}
10. jwt.NewParser
Сигнатура: func NewParser(options ...ParserOption) *Parser
Описание: Создаёт кастомный парсер с дополнительными опциями: пропуск проверки expiration, валидация audience, использование JSON-декодера и т.д.
parser := jwt.NewParser(
jwt.WithValidMethods([]string{"HS256", "HS384"}),
jwt.WithAudience("my-service"),
)
token, err := parser.Parse(tokenString, keyFunc)
Реальный практический пример
Разработаем полноценный HTTP-сервер с аутентификацией через JWT. Сервер будет иметь два эндпоинта: /login (возвращает токен) и /protected (доступен только с валидным токеном).
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
)
// Конфигурация
var (
secretKey = []byte("change-me-in-production-12345")
users = map[string]string{
"admin": "password123",
"user": "pass456",
}
)
// Структура для кастомных claims
type AppClaims struct {
jwt.RegisteredClaims
Username string `json:"username"`
Role string `json:"role"`
}
// Обработчик /login
func loginHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var creds struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&creds); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
// Проверка учетных данных
expectedPass, ok := users[creds.Username]
if !ok || expectedPass != creds.Password {
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
return
}
// Определяем роль
role := "user"
if creds.Username == "admin" {
role = "admin"
}
// Создаем claims
now := time.Now()
claims := AppClaims{
RegisteredClaims: jwt.RegisteredClaims{
Subject: creds.Username,
ExpiresAt: jwt.NewNumericDate(now.Add(2 * time.Hour)),
IssuedAt: jwt.NewNumericDate(now),
Issuer: "my-app",
ID: fmt.Sprintf("%d", now.UnixNano()),
},
Username: creds.Username,
Role: role,
}
// Генерируем токен
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(secretKey)
if err != nil {
log.Printf("Error signing token: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
// Возвращаем токен
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"token": tokenString,
"expires": claims.ExpiresAt.Time.Format(time.RFC3339),
})
}
// Middleware для проверки JWT
func authMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Missing authorization header", http.StatusUnauthorized)
return
}
// Ожидаем формат "Bearer "
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
http.Error(w, "Invalid authorization header format", http.StatusUnauthorized)
return
}
tokenString := parts[1]
// Парсим и верифицируем токен
claims := &AppClaims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, 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 {
http.Error(w, "Invalid or expired token: