gin-jwt
認証ライブラリ
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
}