gin-jwt

認証ライブラリGoGinJWTミドルウェアセキュリティWebフレームワーク

認証ライブラリ

gin-jwt

概要

gin-jwtは、Go言語のGinフレームワーク専用のJWT認証ミドルウェアです。シンプルなAPIでJWTトークンの生成、検証、リフレッシュを実現し、Ginアプリケーションに認証機能を簡単に統合できます。ログイン、ログアウト、認証チェックの標準的なフローを提供し、Ginのミドルウェアパターンに完全に対応しています。2025年現在も活発に開発されており、Go Webアプリケーションで最も人気の高いJWT認証ソリューションの一つです。

詳細

gin-jwtは、JWT(JSON Web Token)認証をGinフレームワークに統合するための専用ミドルウェアです。以下の主な特徴があります:

  • Ginネイティブ統合: Ginフレームワークのミドルウェアパターンに完全対応
  • シンプルなAPI: 最小限の設定でJWT認証を実装可能
  • トークン管理: アクセストークンとリフレッシュトークンの自動管理
  • カスタマイズ可能: トークン生成、検証ロジックの柔軟なカスタマイズ
  • セキュリティ機能: トークン有効期限、署名検証、クレーム検証をサポート
  • RESTful API対応: JSON形式のレスポンスとHTTPステータスコードによる統一的なエラーハンドリング

メリット・デメリット

メリット

  • Ginフレームワークとの完璧な統合により実装が簡単
  • 最小限のボイラープレートコードで認証機能を追加
  • JWT標準に準拠した安全なトークン管理
  • カスタマイズ可能な認証ロジックとクレーム処理
  • RESTful APIに適したJSON形式のレスポンス
  • 活発なコミュニティサポートと豊富なドキュメント

デメリット

  • Ginフレームワーク専用でその他のGoフレームワークでは使用不可
  • 複雑な認証要件(OAuth、SAML等)には対応していない
  • 外部認証プロバイダーとの統合は別途実装が必要
  • トークンの永続化機能は含まれていない

参考ページ

書き方の例

基本的なセットアップ

package main

import (
	"log"
	"net/http"
	"time"

	"github.com/appleboy/gin-jwt/v2"
	"github.com/gin-gonic/gin"
)

type login struct {
	Username string `form:"username" json:"username" binding:"required"`
	Password string `form:"password" json:"password" binding:"required"`
}

type User struct {
	UserName  string `json:"username"`
	FirstName string `json:"firstname"`
	LastName  string `json:"lastname"`
}

func main() {
	r := gin.Default()

	// JWT ミドルウェアの設定
	authMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{
		Realm:       "test zone",
		Key:         []byte("secret key"),
		Timeout:     time.Hour,
		MaxRefresh:  time.Hour,
		IdentityKey: "id",
		PayloadFunc: func(data interface{}) jwt.MapClaims {
			if v, ok := data.(*User); ok {
				return jwt.MapClaims{
					"id":   v.UserName,
					"name": v.FirstName + " " + v.LastName,
				}
			}
			return jwt.MapClaims{}
		},
		IdentityHandler: func(c *gin.Context) interface{} {
			claims := jwt.ExtractClaims(c)
			return &User{
				UserName: claims["id"].(string),
			}
		},
		Authenticator: func(c *gin.Context) (interface{}, error) {
			var loginVals login
			if err := c.ShouldBind(&loginVals); err != nil {
				return "", jwt.ErrMissingLoginValues
			}
			userID := loginVals.Username
			password := loginVals.Password

			// 実際の認証ロジック
			if userID == "admin" && password == "admin" {
				return &User{
					UserName:  userID,
					FirstName: "Admin",
					LastName:  "User",
				}, nil
			}

			return nil, jwt.ErrFailedAuthentication
		},
		Authorizator: func(data interface{}, c *gin.Context) bool {
			if v, ok := data.(*User); ok && v.UserName == "admin" {
				return true
			}
			return false
		},
		Unauthorized: func(c *gin.Context, code int, message string) {
			c.JSON(code, gin.H{
				"code":    code,
				"message": message,
			})
		},
		TokenLookup: "header: Authorization, query: token, cookie: jwt",
		TokenHeadName: "Bearer",
		TimeFunc: time.Now,
	})

	if err != nil {
		log.Fatal("JWT Error:" + err.Error())
	}

	// ルートの設定
	r.POST("/login", authMiddleware.LoginHandler)
	r.NoRoute(authMiddleware.MiddlewareFunc(), func(c *gin.Context) {
		claims := jwt.ExtractClaims(c)
		log.Printf("NoRoute claims: %#v\n", claims)
		c.JSON(404, gin.H{"code": "PAGE_NOT_FOUND", "message": "Page not found"})
	})

	auth := r.Group("/auth")
	auth.GET("/refresh_token", authMiddleware.RefreshHandler)
	auth.Use(authMiddleware.MiddlewareFunc())
	{
		auth.GET("/hello", helloHandler)
		auth.GET("/profile", profileHandler)
	}

	r.Run(":8080")
}

保護されたエンドポイントの実装

func helloHandler(c *gin.Context) {
	claims := jwt.ExtractClaims(c)
	user, _ := c.Get("id")
	c.JSON(200, gin.H{
		"userID": claims["id"],
		"name":   claims["name"],
		"text":   "Hello World.",
		"user":   user.(*User).UserName,
	})
}

func profileHandler(c *gin.Context) {
	user, _ := c.Get("id")
	claims := jwt.ExtractClaims(c)
	
	c.JSON(200, gin.H{
		"user_id":    user.(*User).UserName,
		"first_name": claims["name"],
		"claims":     claims,
		"profile": gin.H{
			"email":       "[email protected]",
			"role":        "administrator",
			"permissions": []string{"read", "write", "delete"},
		},
	})
}

ロールベースのアクセス制御

type Role string

const (
	AdminRole Role = "admin"
	UserRole  Role = "user"
	GuestRole Role = "guest"
)

func createJWTMiddleware() *jwt.GinJWTMiddleware {
	return &jwt.GinJWTMiddleware{
		Realm:      "api",
		Key:        []byte("your-secret-key"),
		Timeout:    time.Hour * 24,
		MaxRefresh: time.Hour * 24 * 7,
		
		PayloadFunc: func(data interface{}) jwt.MapClaims {
			if user, ok := data.(*User); ok {
				return jwt.MapClaims{
					"id":   user.UserName,
					"role": user.Role,
					"exp":  time.Now().Add(time.Hour * 24).Unix(),
				}
			}
			return jwt.MapClaims{}
		},
		
		Authorizator: func(data interface{}, c *gin.Context) bool {
			requiredRole := c.GetString("required_role")
			if requiredRole == "" {
				return true // ロール制限なし
			}
			
			claims := jwt.ExtractClaims(c)
			userRole := claims["role"].(string)
			
			// ロール階層チェック
			switch requiredRole {
			case "admin":
				return userRole == "admin"
			case "user":
				return userRole == "admin" || userRole == "user"
			default:
				return true
			}
		},
	}
}

// ロール制限ミドルウェア
func requireRole(role string) gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Set("required_role", role)
		c.Next()
	}
}

// 使用例
func setupRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware) {
	api := r.Group("/api")
	api.Use(authMiddleware.MiddlewareFunc())
	{
		// 全ユーザーアクセス可能
		api.GET("/profile", profileHandler)
		
		// ユーザー以上必要
		api.GET("/data", requireRole("user"), dataHandler)
		
		// 管理者のみ
		api.POST("/admin/users", requireRole("admin"), createUserHandler)
		api.DELETE("/admin/users/:id", requireRole("admin"), deleteUserHandler)
	}
}

カスタム認証プロバイダーとの統合

type AuthService struct {
	db *sql.DB
}

func (a *AuthService) AuthenticateUser(username, password string) (*User, error) {
	// データベース認証
	row := a.db.QueryRow("SELECT id, username, password_hash, role FROM users WHERE username = ?", username)
	
	var user User
	var passwordHash string
	err := row.Scan(&user.ID, &user.UserName, &passwordHash, &user.Role)
	if err != nil {
		return nil, errors.New("user not found")
	}
	
	// パスワード検証
	if !checkPasswordHash(password, passwordHash) {
		return nil, errors.New("invalid password")
	}
	
	return &user, nil
}

func setupJWTWithDB(authService *AuthService) *jwt.GinJWTMiddleware {
	return &jwt.GinJWTMiddleware{
		Authenticator: func(c *gin.Context) (interface{}, error) {
			var loginVals login
			if err := c.ShouldBind(&loginVals); err != nil {
				return "", jwt.ErrMissingLoginValues
			}
			
			user, err := authService.AuthenticateUser(loginVals.Username, loginVals.Password)
			if err != nil {
				return nil, jwt.ErrFailedAuthentication
			}
			
			// ログイン記録
			go authService.LogLogin(user.ID, c.ClientIP())
			
			return user, nil
		},
		
		LogoutResponse: func(c *gin.Context, code int) {
			// ログアウト時の処理
			claims := jwt.ExtractClaims(c)
			if userID, exists := claims["id"]; exists {
				go authService.LogLogout(userID.(string), c.ClientIP())
			}
			
			c.JSON(http.StatusOK, gin.H{
				"code":    http.StatusOK,
				"message": "Successfully logged out",
			})
		},
	}
}

エラーハンドリングとセキュリティ

func setupSecureJWT() *jwt.GinJWTMiddleware {
	return &jwt.GinJWTMiddleware{
		// セキュアな設定
		SecureCookie:    true, // HTTPS必須
		CookieHTTPOnly:  true, // XSS対策
		CookieSameSite:  http.SameSiteStrictMode,
		CookieName:      "jwt-token",
		
		// エラーハンドリング
		HTTPStatusMessageFunc: func(e error, c *gin.Context) string {
			switch e {
			case jwt.ErrFailedAuthentication:
				return "Invalid username or password"
			case jwt.ErrFailedTokenCreation:
				return "Failed to create token"
			case jwt.ErrExpiredToken:
				return "Token has expired"
			case jwt.ErrEmptyAuthHeader:
				return "Authorization header is required"
			default:
				return "Authentication failed"
			}
		},
		
		// カスタムクレーム検証
		ClaimsValidator: func(claims jwt.MapClaims) error {
			// 発行者チェック
			if claims["iss"] != "your-app" {
				return errors.New("invalid issuer")
			}
			
			// オーディエンスチェック
			if claims["aud"] != "your-api" {
				return errors.New("invalid audience")
			}
			
			return nil
		},
		
		// レート制限
		LoginResponse: func(c *gin.Context, code int, token string, expire time.Time) {
			// レート制限チェック
			clientIP := c.ClientIP()
			if isRateLimited(clientIP) {
				c.JSON(http.StatusTooManyRequests, gin.H{
					"code":    http.StatusTooManyRequests,
					"message": "Too many login attempts",
				})
				return
			}
			
			c.JSON(http.StatusOK, gin.H{
				"code":   http.StatusOK,
				"token":  token,
				"expire": expire.Format(time.RFC3339),
			})
		},
	}
}

// レート制限の実装例
var loginAttempts = make(map[string][]time.Time)
var mutex = sync.RWMutex{}

func isRateLimited(ip string) bool {
	mutex.Lock()
	defer mutex.Unlock()
	
	now := time.Now()
	cutoff := now.Add(-time.Minute * 15) // 15分間の制限
	
	// 古い記録を削除
	attempts := loginAttempts[ip]
	var validAttempts []time.Time
	for _, attempt := range attempts {
		if attempt.After(cutoff) {
			validAttempts = append(validAttempts, attempt)
		}
	}
	
	// 新しい試行を追加
	validAttempts = append(validAttempts, now)
	loginAttempts[ip] = validAttempts
	
	// 制限チェック(15分間に5回まで)
	return len(validAttempts) > 5
}