Go の Gin での認証・認可
Web API において認証(Authentication)と認可(Authorization)は重要なセキュリティ要素だ。Gin では JWT を使った認証システムを比較的簡単に実装できる。
認証と認可の違い
認証(Authentication)
「誰であるか」を確認すること。ログイン処理でユーザーの身元を検証する。
認可(Authorization)
「何ができるか」を確認すること。認証済みユーザーが特定のリソースにアクセスできるか判定する。
JWT の基本
JWT(JSON Web Token)は、ユーザー情報を含んだトークンだ。サーバーはセッションを保持せずにユーザーを識別できる。
go get -u github.com/golang-jwt/jwt/v5JWT の生成
package auth
import (
"time"
"github.com/golang-jwt/jwt/v5"
)
var jwtSecret = []byte("your-secret-key")
type Claims struct {
UserID uint `json:"user_id"`
Email string `json:"email"`
Role string `json:"role"`
jwt.RegisteredClaims
}
func GenerateToken(userID uint, email, role string) (string, error) {
claims := Claims{
UserID: userID,
Email: email,
Role: role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret)
}JWT の検証
func ValidateToken(tokenString string) (*Claims, error) {
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 jwtSecret, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, fmt.Errorf("invalid token")
}ログインハンドラ
type LoginInput struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
}
func Login(c *gin.Context) {
var input LoginInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// ユーザー検索
var user models.User
if err := database.DB.Where("email = ?", input.Email).First(&user).Error; err != nil {
c.JSON(401, gin.H{"error": "Invalid credentials"})
return
}
// パスワード検証
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(input.Password)); err != nil {
c.JSON(401, gin.H{"error": "Invalid credentials"})
return
}
// トークン生成
token, err := auth.GenerateToken(user.ID, user.Email, user.Role)
if err != nil {
c.JSON(500, gin.H{"error": "Failed to generate token"})
return
}
c.JSON(200, gin.H{"token": token})
}認証ミドルウェア
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.AbortWithStatusJSON(401, gin.H{"error": "Authorization header required"})
return
}
// "Bearer <token>" 形式を想定
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
c.AbortWithStatusJSON(401, gin.H{"error": "Invalid authorization format"})
return
}
claims, err := auth.ValidateToken(parts[1])
if err != nil {
c.AbortWithStatusJSON(401, gin.H{"error": "Invalid or expired token"})
return
}
// コンテキストにユーザー情報を設定
c.Set("userID", claims.UserID)
c.Set("email", claims.Email)
c.Set("role", claims.Role)
c.Next()
}
}認可ミドルウェア(ロールベース)
特定のロールを持つユーザーだけがアクセスできるようにする。
func RoleRequired(roles ...string) gin.HandlerFunc {
return func(c *gin.Context) {
userRole, exists := c.Get("role")
if !exists {
c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"})
return
}
roleStr := userRole.(string)
for _, role := range roles {
if roleStr == role {
c.Next()
return
}
}
c.AbortWithStatusJSON(403, gin.H{"error": "Forbidden"})
}
}ルーティングへの適用
func main() {
r := gin.Default()
// 公開ルート
r.POST("/login", handlers.Login)
r.POST("/register", handlers.Register)
// 認証が必要なルート
authorized := r.Group("/")
authorized.Use(AuthMiddleware())
{
authorized.GET("/profile", handlers.GetProfile)
authorized.PUT("/profile", handlers.UpdateProfile)
// 管理者専用ルート
admin := authorized.Group("/admin")
admin.Use(RoleRequired("admin"))
{
admin.GET("/users", handlers.AdminGetUsers)
admin.DELETE("/users/:id", handlers.AdminDeleteUser)
}
}
r.Run(":8080")
}ヘルパー関数
ハンドラからユーザー情報を取得するヘルパーを用意すると便利だ。
func GetCurrentUserID(c *gin.Context) (uint, error) {
userID, exists := c.Get("userID")
if !exists {
return 0, errors.New("user not found in context")
}
return userID.(uint), nil
}
// 使用例
func GetProfile(c *gin.Context) {
userID, err := GetCurrentUserID(c)
if err != nil {
c.JSON(401, gin.H{"error": "Unauthorized"})
return
}
var user models.User
database.DB.First(&user, userID)
c.JSON(200, user)
}リフレッシュトークン
アクセストークンの有効期限を短くし、リフレッシュトークンで更新する方式もある。
アクセストークン
有効期限が短い(15分〜1時間)。API アクセスに使用する。
リフレッシュトークン
有効期限が長い(数日〜数週間)。アクセストークンの再発行に使用する。データベースに保存して管理する。
func RefreshToken(c *gin.Context) {
refreshToken := c.PostForm("refresh_token")
// リフレッシュトークンを検証(DBから取得して比較)
var storedToken models.RefreshToken
if err := database.DB.Where("token = ?", refreshToken).First(&storedToken).Error; err != nil {
c.JSON(401, gin.H{"error": "Invalid refresh token"})
return
}
// 新しいアクセストークンを発行
newToken, _ := auth.GenerateToken(storedToken.UserID, storedToken.Email, storedToken.Role)
c.JSON(200, gin.H{"token": newToken})
}セキュリティのベストプラクティス
JWT シークレットは環境変数で管理し、ハードコードしない
HTTPS を必ず使用してトークンの盗聴を防ぐ
トークンの有効期限を適切に設定する
パスワードは bcrypt などでハッシュ化して保存する
ログアウト時はクライアント側でトークンを削除し、必要に応じてブラックリストを実装する
認証・認可は API セキュリティの基盤だ。要件に応じて OAuth2 や OpenID Connect などの標準プロトコルの採用も検討するとよい。