高度なサンプル
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 ファイルから鍵をロード
本番環境では通常、非対称鍵を 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)
}その他のサンプル
- Web サーバー統合 — 認証ミドルウェア、RBAC、リフレッシュ、ログアウト、グレースフルシャットダウン
- 基本サンプル — HMAC、トークンペア、失効、レート制限