Продвинутые примеры
Асимметричная подпись RSA
Использование RSA приватного ключа для подписи и публичного для проверки. Подходит для микросервисной архитектуры, где проверяющей стороне не нужен приватный ключ.
package main
import (
"crypto/rand"
"crypto/rsa"
"fmt"
"log"
"github.com/cybergodev/jwt"
)
func main() {
// Генерация пары RSA-ключей (в реальном использовании загружайте из файла)
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
log.Fatal(err)
}
cfg := jwt.DefaultConfig()
cfg.SigningMethod = jwt.SigningMethodRS256
cfg.SigningKey = privateKey
cfg.VerificationKey = &privateKey.PublicKey // Необязательно, если не указано, используется SigningKey
processor, err := jwt.New(cfg)
if err != nil {
panic(err)
}
defer processor.Close()
claims := &jwt.Claims{UserID: "user456", Username: "bob"}
token, err := processor.Create(claims)
if err != nil {
panic(err)
}
fmt.Println("RSA Token:", token)
parsed, valid, err := processor.Validate(token)
if err != nil {
panic(err)
}
fmt.Println("Valid:", valid)
fmt.Println("UserID:", parsed.UserID)
}Подпись RSA-PSS
RSA-PSS (современная замена RS256/384/512) использует заполнение вероятностной схемы подписи (PSS), безопаснее PKCS#1 v1.5. Ключи полностью идентичны RSA, дополнительная генерация не требуется.
package main
import (
"crypto/rand"
"crypto/rsa"
"fmt"
"log"
"github.com/cybergodev/jwt"
)
func main() {
// Генерация пары RSA-ключей (RSA-PSS и RSA используют один тип ключей)
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
log.Fatal(err)
}
cfg := jwt.DefaultConfig()
cfg.SigningMethod = jwt.SigningMethodPS256
cfg.SigningKey = privateKey
cfg.VerificationKey = &privateKey.PublicKey
processor, err := jwt.New(cfg)
if err != nil {
panic(err)
}
defer processor.Close()
claims := &jwt.Claims{UserID: "user_ps", Username: "diana"}
token, err := processor.Create(claims)
if err != nil {
panic(err)
}
fmt.Println("RSA-PSS Token:", token)
parsed, valid, err := processor.Validate(token)
if err != nil {
panic(err)
}
fmt.Println("Valid:", valid) // Вывод: Valid: true
fmt.Println("UserID:", parsed.UserID)
}Рекомендуемая замена RSA
В новых проектах рекомендуется отдавать приоритет RSA-PSS (PS256/384/512). Заполнение PSS обеспечивает более сильную доказуемую безопасность, чем PKCS#1 v1.5, а ключи полностью совместимы с RSA.
Асимметричная подпись ECDSA
Использование ECDSA на эллиптических кривых — более короткие ключи и лучшая производительность.
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"fmt"
"log"
"github.com/cybergodev/jwt"
)
func main() {
// Генерация пары ECDSA-ключей
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
log.Fatal(err)
}
cfg := jwt.DefaultConfig()
cfg.SigningMethod = jwt.SigningMethodES256
cfg.SigningKey = privateKey
processor, err := jwt.New(cfg)
if err != nil {
panic(err)
}
defer processor.Close()
claims := &jwt.Claims{UserID: "user789", Username: "charlie"}
token, err := processor.Create(claims)
if err != nil {
panic(err)
}
fmt.Println("ECDSA Token:", token)
}Режим разделения ключей
Имитация межсервисной проверки токенов в микросервисной архитектуре: сервис аутентификации держит приватный ключ и выпускает токены, API-сервис проверяет токен через публичный ключ.
package main
import (
"crypto/rand"
"crypto/rsa"
"fmt"
"log"
"github.com/cybergodev/jwt"
)
func main() {
// Генерация пары RSA-ключей
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
log.Fatal(err)
}
publicKey := &privateKey.PublicKey
// --- Сервис аутентификации: держит приватный ключ, выпускает токены ---
authCfg := jwt.DefaultConfig()
authCfg.SigningMethod = jwt.SigningMethodRS256
authCfg.SigningKey = privateKey
authCfg.Issuer = "auth-service"
authProcessor, err := jwt.New(authCfg)
if err != nil {
panic(err)
}
defer authProcessor.Close()
claims := &jwt.Claims{UserID: "user_dist", Username: "charlie"}
token, err := authProcessor.Create(claims)
if err != nil {
panic(err)
}
fmt.Println("Сервис аутентификации выпустил токен (приватный ключ)")
// --- API-сервис: проверяет токен через публичный ключ ---
apiCfg := jwt.DefaultConfig()
apiCfg.SigningMethod = jwt.SigningMethodRS256
apiCfg.SigningKey = privateKey // Текущий API требует SigningKey непустым
apiCfg.VerificationKey = publicKey // При проверке фактически используется этот публичный ключ
apiCfg.Issuer = "auth-service" // Должен совпадать с издателем
apiProcessor, err := jwt.New(apiCfg)
if err != nil {
panic(err)
}
defer apiProcessor.Close()
parsed, valid, err := apiProcessor.Validate(token)
if err != nil {
panic(err)
}
fmt.Println("API-сервис проверка пройдена (публичный ключ):", valid) // Вывод: API-сервис проверка пройдена (публичный ключ): true
fmt.Println("UserID:", parsed.UserID)
}SigningKey обязателен
Текущий API требует, чтобы SigningKey было непустым (принудительная проверка на этапе валидации), поэтому в конфигурации API-сервиса приватный ключ всё равно должен быть записан. Но после установки VerificationKey процесс проверки использует только публичный ключ. Processor, настроенный только на проверку, не должен вызывать Create / CreateRefresh.
Загрузка ключей из PEM-файла
В production асимметричные ключи обычно хранятся в PEM-файлах. Следующий пример показывает, как загрузить приватный ключ через pem.Decode + x509.ParsePKCS8PrivateKey и публичный через x509.ParsePKIXPublicKey.
package main
import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"os"
"github.com/cybergodev/jwt"
)
func main() {
// --- Загрузка приватного RSA-ключа ---
keyData, err := os.ReadFile("private_key.pem")
if err != nil {
fmt.Println("Ошибка чтения приватного ключа:", err)
return
}
block, _ := pem.Decode(keyData)
if block == nil {
fmt.Println("Ошибка декодирования PEM приватного ключа")
return
}
parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
fmt.Println("Ошибка разбора приватного ключа:", err)
return
}
privateKey, ok := parsedKey.(*rsa.PrivateKey)
if !ok {
fmt.Println("Тип ключа не RSA")
return
}
// --- Загрузка публичного RSA-ключа ---
pubData, err := os.ReadFile("public_key.pem")
if err != nil {
fmt.Println("Ошибка чтения публичного ключа:", err)
return
}
pubBlock, _ := pem.Decode(pubData)
if pubBlock == nil {
fmt.Println("Ошибка декодирования PEM публичного ключа")
return
}
parsedPub, err := x509.ParsePKIXPublicKey(pubBlock.Bytes)
if err != nil {
fmt.Println("Ошибка разбора публичного ключа:", err)
return
}
publicKey, ok := parsedPub.(*rsa.PublicKey)
if !ok {
fmt.Println("Тип публичного ключа не RSA")
return
}
// --- Настройка Processor ---
cfg := jwt.DefaultConfig()
cfg.SigningMethod = jwt.SigningMethodRS256
cfg.SigningKey = privateKey
cfg.VerificationKey = publicKey
processor, err := jwt.New(cfg)
if err != nil {
fmt.Println("Ошибка инициализации:", err)
return
}
defer processor.Close()
fmt.Println("Ключи загружены из PEM-файла") // Вывод: Ключи загружены из PEM-файла
}Формат ключей
- Заголовок PEM приватного ключа —
-----BEGIN PRIVATE KEY-----(PKCS#8) или-----BEGIN RSA PRIVATE KEY-----(PKCS#1). PKCS#8 разбирается черезx509.ParsePKCS8PrivateKey, PKCS#1 — черезx509.ParsePKCS1PrivateKey. - Заголовок PEM публичного ключа —
-----BEGIN PUBLIC KEY-----, разбирается черезx509.ParsePKIXPublicKey. ParsePKCS8PrivateKey/ParsePKIXPublicKeyвозвращаютanyи требуют приведения типа к*rsa.PrivateKey/*rsa.PublicKey(для ECDSA аналогично — к*ecdsa.PrivateKey/*ecdsa.PublicKey).
Пользовательские Claims
Определение собственной структуры Claims с бизнес-полями.
package main
import (
"errors"
"fmt"
"github.com/cybergodev/jwt"
)
// Пользовательская структура Claims
type MyClaims struct {
UserID string `json:"user_id"`
Email string `json:"email"`
Role string `json:"role"`
jwt.RegisteredClaims
}
func (c *MyClaims) GetRegisteredClaims() *jwt.RegisteredClaims {
return &c.RegisteredClaims
}
func (c *MyClaims) Validate() error {
if c.UserID == "" {
return errors.New("user_id is required")
}
if c.Email == "" {
return errors.New("email is required")
}
return nil
}
func main() {
cfg := jwt.DefaultConfig()
cfg.SecretKey = "hmac-key-that-has-at-least-32-bytes!"
processor, err := jwt.New(cfg)
if err != nil {
panic(err)
}
defer processor.Close()
claims := &MyClaims{
UserID: "user123",
Email: "[email protected]",
Role: "admin",
}
// Создание токена
token, err := processor.Create(claims)
if err != nil {
panic(err)
}
fmt.Println("Token:", token)
// Проверка в пользовательские Claims
myClaims := &MyClaims{}
result, valid, err := processor.ValidateInto(token, myClaims)
if err != nil {
panic(err)
}
if valid {
parsed := result.(*MyClaims)
fmt.Println("UserID:", parsed.UserID) // Вывод: user123
fmt.Println("Email:", parsed.Email) // Вывод: [email protected]
}
// Обновление в пользовательские Claims
refreshToken, err := processor.CreateRefresh(claims)
if err != nil {
panic(err)
}
newToken, err := processor.RefreshInto(refreshToken, &MyClaims{})
if err != nil {
panic(err)
}
fmt.Println("New Token:", newToken)
}Пользовательский бэкенд чёрного списка (Redis)
package main
import (
"context"
"fmt"
"time"
"github.com/cybergodev/jwt"
)
// RedisBlacklistStore реализует интерфейс BlacklistStore
// Примечание: при реальном использовании необходим Redis-клиент (например, github.com/redis/go-redis)
type RedisBlacklistStore struct {
// client *redis.Client
}
func (s *RedisBlacklistStore) Add(tokenID string, expiresAt time.Time) error {
ttl := time.Until(expiresAt)
if ttl <= 0 {
return nil
}
// return s.client.Set(context.Background(), "blacklist:"+tokenID, "1", ttl).Err()
fmt.Printf("Redis ADD: %s, TTL: %v\n", tokenID, ttl)
return nil
}
func (s *RedisBlacklistStore) Contains(tokenID string) (bool, error) {
// return s.client.Exists(context.Background(), "blacklist:"+tokenID).Result()
return false, nil
}
func (s *RedisBlacklistStore) Close() error {
// return s.client.Close()
return nil
}
func main() {
_ = context.Background() // Сохраняем импорт context доступным (раскомментируйте Redis-вызовы при реальном использовании)
cfg := jwt.DefaultConfig()
cfg.SecretKey = "hmac-key-that-has-at-least-32-bytes!"
cfg.Blacklist.Store = &RedisBlacklistStore{}
processor, err := jwt.New(cfg)
if err != nil {
panic(err)
}
defer processor.Close()
claims := &jwt.Claims{UserID: "user123", Username: "alice"}
token, err := processor.Create(claims)
if err != nil {
panic(err)
}
err = processor.Revoke(token)
if err != nil {
panic(err)
}
fmt.Println("Token revoked via Redis backend")
}Внедрение часов (сценарий тестирования)
Использование FixedClock для контроля времени в тестах.
package main
import (
"fmt"
"time"
"github.com/cybergodev/jwt"
)
func main() {
fixedTime := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
cfg := jwt.DefaultConfig()
cfg.SecretKey = "hmac-key-that-has-at-least-32-bytes!"
cfg.Clock = jwt.FixedClock{T: fixedTime}
processor, err := jwt.New(cfg)
if err != nil {
panic(err)
}
defer processor.Close()
claims := &jwt.Claims{UserID: "user123", Username: "alice"}
token, err := processor.Create(claims)
if err != nil {
panic(err)
}
parsed, _, err := processor.Validate(token)
if err != nil {
panic(err)
}
fmt.Println("IssuedAt:", parsed.IssuedAt.Time) // Вывод: 2026-01-01 00:00:00 +0000 UTC
fmt.Println("ExpiresAt:", parsed.ExpiresAt.Time) // Вывод: 2026-01-01 00:15:00 +0000 UTC
}Разбор непроверенного токена
Извлечение информации из Claims без проверки подписи для отладки или логирования.
package main
import (
"fmt"
"github.com/cybergodev/jwt"
)
func main() {
cfg := jwt.DefaultConfig()
cfg.SecretKey = "hmac-key-that-has-at-least-32-bytes!"
processor, err := jwt.New(cfg)
if err != nil {
panic(err)
}
defer processor.Close()
claims := &jwt.Claims{UserID: "user123", Username: "alice"}
token, err := processor.Create(claims)
if err != nil {
panic(err)
}
// Разбор без проверки подписи
parsed := &jwt.Claims{}
err = processor.ParseUnverified(token, parsed)
if err != nil {
panic(err)
}
fmt.Println("UserID (unverified):", parsed.UserID)
}Дополнительные примеры
- Интеграция с веб-сервером — промежуточное ПО аутентификации, RBAC, обновление, выход, плавное завершение
- Базовые примеры — HMAC, пары токенов, отзыв, rate limiting