Skip to content

Go-DDDマーケットプレイスアプリケーション Echo JWT認証の詳細解説

1. 概要

この文書では、Go-DDDマーケットプレイスアプリケーションにおけるJWT(JSON Web Token)を使用した認証機能の実装について詳しく解説します。JWTは、クライアントとサーバー間で安全に情報を交換するための、コンパクトで自己完結型のトークン形式です。

1.1 JWT認証の役割

JWT認証は以下の役割を担います:

  1. ユーザー認証: ユーザーのログイン情報を検証し、有効な認証トークンを発行します。
  2. アクセス制御: 保護されたリソースへのアクセスを制限し、認証されたユーザーのみがアクセスできるようにします。
  3. ステートレス認証: サーバー側でセッション状態を保持せず、トークン自体に必要な情報を含めることでステートレスな認証を実現します。
  4. クロスドメイン認証: 異なるドメイン間でも認証情報を安全に共有できます。

1.2 アーキテクチャにおける位置づけ

以下の図は、JWT認証のアーキテクチャにおける位置づけを示しています:

uml diagram

2. JWT認証の実装

以下はJWT認証実装の概要を表すフロー図です:

uml diagram

2.1 JWT設定

2.1.1 JWT設定の定義

JWT認証に必要な設定を定義します。

package config

import (
    "time"
)

// JWTConfig はJWT認証の設定
type JWTConfig struct {
    SecretKey     string
    TokenExpiry   time.Duration
    SigningMethod string
}

// NewJWTConfig はJWT設定を作成する
func NewJWTConfig() *JWTConfig {
    return &JWTConfig{
        SecretKey:     "your-secret-key", // 本番環境では環境変数から取得するなど安全に管理する
        TokenExpiry:   24 * time.Hour,    // トークンの有効期限(24時間)
        SigningMethod: "HS256",           // 署名アルゴリズム
    }
}

2.1.2 認証ミドルウェアの設定

Echoフレームワークに認証ミドルウェアを設定します。

package rest

import (
    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
    "github.com/sklinkert/go-ddd/internal/config"
)

// SetupAuthMiddleware はJWT認証ミドルウェアを設定する
func SetupAuthMiddleware(e *echo.Echo, jwtConfig *config.JWTConfig) {
    // JWT設定
    config := middleware.JWTConfig{
        SigningKey:  []byte(jwtConfig.SecretKey),
        TokenLookup: "header:Authorization",
        AuthScheme:  "Bearer",
    }

    // 認証が必要なルートグループ
    auth := e.Group("/api/v1/auth")
    auth.Use(middleware.JWTWithConfig(config))

    // 認証が必要なエンドポイントを追加
    auth.GET("/profile", getProfile)
    auth.GET("/orders", getOrders)
    // その他の保護されたエンドポイント
}

2.2 ユーザー認証

2.2.1 ログインハンドラの実装

ユーザーのログイン処理を行うハンドラを実装します。

package rest

import (
    "github.com/dgrijalva/jwt-go"
    "github.com/labstack/echo/v4"
    "github.com/sklinkert/go-ddd/internal/application/services"
    "github.com/sklinkert/go-ddd/internal/config"
    "net/http"
    "time"
)

type AuthController struct {
    userService *services.UserService
    jwtConfig   *config.JWTConfig
}

func NewAuthController(e *echo.Echo, userService *services.UserService, jwtConfig *config.JWTConfig) {
    controller := &AuthController{
        userService: userService,
        jwtConfig:   jwtConfig,
    }

    // 認証エンドポイント
    e.POST("/api/v1/login", controller.Login)
}

// Login はユーザーログイン処理を行う
func (c *AuthController) Login(ctx echo.Context) error {
    // リクエストからユーザー情報を取得
    var req struct {
        Email    string `json:"email"`
        Password string `json:"password"`
    }

    if err := ctx.Bind(&req); err != nil {
        return ctx.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request"})
    }

    // ユーザー認証
    user, err := c.userService.Authenticate(req.Email, req.Password)
    if err != nil {
        return ctx.JSON(http.StatusUnauthorized, map[string]string{"error": "Invalid credentials"})
    }

    // JWTトークンの生成
    token, err := c.generateToken(user.ID, user.Email)
    if err != nil {
        return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to generate token"})
    }

    // レスポンス
    return ctx.JSON(http.StatusOK, map[string]string{
        "token": token,
        "type":  "Bearer",
    })
}

// generateToken はJWTトークンを生成する
func (c *AuthController) generateToken(userID, email string) (string, error) {
    // トークンの有効期限
    expiry := time.Now().Add(c.jwtConfig.TokenExpiry)

    // クレームの作成
    claims := jwt.MapClaims{
        "id":    userID,
        "email": email,
        "exp":   expiry.Unix(),
    }

    // トークンの作成
    token := jwt.NewWithClaims(jwt.GetSigningMethod(c.jwtConfig.SigningMethod), claims)

    // トークンの署名
    return token.SignedString([]byte(c.jwtConfig.SecretKey))
}

2.2.2 ユーザー検証の実装

ユーザーサービスでの認証処理を実装します。

package services

import (
    "errors"
    "github.com/sklinkert/go-ddd/internal/domain/entities"
    "github.com/sklinkert/go-ddd/internal/domain/repositories"
    "golang.org/x/crypto/bcrypt"
)

type UserService struct {
    userRepository repositories.UserRepository
}

func NewUserService(userRepository repositories.UserRepository) *UserService {
    return &UserService{
        userRepository: userRepository,
    }
}

// Authenticate はユーザーの認証を行う
func (s *UserService) Authenticate(email, password string) (*entities.User, error) {
    // メールアドレスでユーザーを検索
    user, err := s.userRepository.FindByEmail(email)
    if err != nil {
        return nil, errors.New("user not found")
    }

    // パスワードの検証
    err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password))
    if err != nil {
        return nil, errors.New("invalid password")
    }

    return user, nil
}

2.3 保護されたリソースへのアクセス

2.3.1 認証済みユーザーのプロファイル取得

認証が必要なエンドポイントの実装例です。

// getProfile は認証済みユーザーのプロファイル情報を返す
func getProfile(c echo.Context) error {
    // JWTトークンからユーザー情報を取得
    user := c.Get("user").(*jwt.Token)
    claims := user.Claims.(jwt.MapClaims)
    userID := claims["id"].(string)
    email := claims["email"].(string)

    // ユーザープロファイルを返す
    return c.JSON(http.StatusOK, map[string]interface{}{
        "id":    userID,
        "email": email,
        "message": "認証済みユーザー",
    })
}

// getOrders は認証済みユーザーの注文履歴を返す
func getOrders(c echo.Context) error {
    // JWTトークンからユーザーIDを取得
    user := c.Get("user").(*jwt.Token)
    claims := user.Claims.(jwt.MapClaims)
    userID := claims["id"].(string)

    // ユーザーの注文履歴を取得(実際の実装ではサービスを呼び出す)
    orders := []map[string]interface{}{
        {"id": "order1", "amount": 1000, "date": "2023-01-01"},
        {"id": "order2", "amount": 2000, "date": "2023-01-15"},
    }

    return c.JSON(http.StatusOK, map[string]interface{}{
        "user_id": userID,
        "orders":  orders,
    })
}

3. セキュリティ考慮事項

3.1 トークンの安全性

JWT認証を安全に実装するための考慮事項:

  1. シークレットキーの保護: シークレットキーは環境変数や安全な設定管理システムで管理し、ソースコードに直接記述しない。
  2. トークンの有効期限: 短い有効期限を設定し、長期間のアクセスにはリフレッシュトークンを使用する。
  3. 適切な署名アルゴリズム: HS256やRS256などの安全な署名アルゴリズムを使用する。
  4. センシティブ情報の保護: パスワードなどのセンシティブな情報はJWTに含めない。

3.2 CSRF対策

クロスサイトリクエストフォージェリ(CSRF)攻撃からの保護:

// CSRFミドルウェアの設定
e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
    TokenLookup: "header:X-CSRF-Token",
    CookieName:  "csrf",
    CookiePath:  "/",
    Skipper: func(c echo.Context) bool {
        // APIエンドポイントはCSRF保護をスキップ
        return strings.HasPrefix(c.Path(), "/api/")
    },
}))

3.3 リフレッシュトークンの実装

長期間のセッション維持のためのリフレッシュトークン実装:

```go // リフレッシュトークンの生成 func (c *AuthController) generateRefreshToken(userID string) (string, error) { // リフレッシュトークンの有効期限(30日) expiry := time.Now().Add(30 * 24 * time.Hour)

claims := jwt.MapClaims{
    "id":  userID,
    "exp": expiry.Unix(),
    "type": "refresh",
}