GoミドルウェアとJWT認証

ミドルウェアとは、Goのマイクロサービスにおける「パイプライン」のことです。認証、レート制限、ロギング、リカバリなど、各横断的関心事項は、それぞれミドルウェアの一要素となります。

すべてのAPIエンドポイントでJWT認証の検証、アクティビティのログ記録、またはレート制限の適用が必要な場合、ミドルウェアパターンを利用すれば、コードを一度記述するだけで、すべてのエンドポイントに適用することができます。

1. 学習内容


2. バックエンドエンジニアの実話

(1) 課題:各ルーターでトークンを手動で確認する必要があること。私は3回も確認し忘れてしまった。

チャーリーはAPI Gatewayチームに所属しており、すべてのエンドポイントの認証を担当しています:

「上司から、すべてのAPIでJWTトークンの検証を行うよう指示があった。そこで、各ハンドラーに15行のトークン検証コードをコピー&ペーストした。デプロイから2日目、/admin/usersへの認証処理を追加し忘れていたため、誰でもユーザーを直接削除できてしまった。CTOは『セキュリティ監査で重大な脆弱性が発見された』と言った。」

GO
// 悪いコード:各ハンドラで手動検証
func deleteUser(w http.ResponseWriter, r *http.Request) {
    // Token 検証の書き忘れ!重大なセキュリティ脆弱性
    userID := r.PathValue("id")
    deleteUserFromDB(userID)
}

func updateOrder(w http.ResponseWriter, r *http.Request) {
    token := r.Header.Get("Authorization")
    // 毎回 15 行の検証コードをコピペ
    if !validateToken(token) {
        http.Error(w, "unauthorized", 401)
        return
    }
    // ビジネスロジック...
}

(2) Goソリューション:認証ミドルウェア

GO
// 良いコード:認証ミドルウェア、一度実装すればどこでも有効
func AuthMiddleware(jwtSecret 文字列) Middleware {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            token := extractToken(r)
            claims, err := validateJWT(token, jwtSecret)
            if err != nil {
                writeError(w, http.StatusUnauthorized, "invalid token")
                return
            }
            // ユーザー情報を Context に注入
            ctx := context.WithValue(r.Context(), "ユーザー", claims)
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

// 登録は Middleware を 1 つ追加するだけ
mux.Handle("POST /api/orders", AuthMiddleware(secret)(orderHandler))

(3) メリット:手動とミドルウェアの比較

次元 手動チェック ミドルウェアパターン
漏れのリスク 新しいルートを追加し忘れるリスクがある ゼロ
コードの重複 ハンドラあたり15行 0行(ハンドラはビジネスロジックにのみ焦点を当てている)
ロジックの変更 20個のハンドラの変更 1個のミドルウェアの変更
ユニットテスト 各ハンドラの認証をテストする ミドルウェアは1回だけテストする

3. JWT認証

▶ サンプル:JWTの生成と検証

GO
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "encoding/json"
    "fmt"
    "strings"
    "time"
)

// ---------- JWT 手動実装(サードパーティライブラリ非依存)----------

type JWTClaims struct {
    UserID   int      `json:"user_id"`
    Role     string   `json:"role"`
    Permissions []string `json:"permissions"`
    ExpiresAt int64   `json:"exp"`
}

type JWTHeader struct {
    Alg string `json:"alg"`
    Typ string `json:"typ"`
}

func base64Encode(data []byte) string {
    return strings.TrimRight(base64.URLEncoding.EncodeToString(data), "=")
}

func base64Decode(s string) ([]byte, error) {
    // padding を補完
    switch len(s) % 4 {
    case 2:
        s += "=="
    case 3:
        s += "="
    }
    return base64.URLEncoding.DecodeString(s)
}

func createJWT(claims JWTClaims, secret string) (string, error) {
    header := JWTHeader{Alg: "HS256", Typ: "JWT"}
    headerJSON, _ := json.Marshal(header)
    claimsJSON, _ := json.Marshal(claims)

    headerEnc := base64Encode(headerJSON)
    claimsEnc := base64Encode(claimsJSON)

    // 署名
    message := headerEnc + "." + claimsEnc
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(message))
    signature := base64Encode(mac.Sum(nil))

    return message + "." + signature, nil
}

func validateJWT(token string, secret string) (*JWTClaims, error) {
    parts := strings.Split(token, ".")
    if len(parts) != 3 {
        return nil, fmt.Errorf("invalid token format")
    }

    // 署名の検証
    message := parts[0] + "." + parts[1]
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(message))
    expectedSig := base64Encode(mac.Sum(nil))

    if !hmac.Equal([]byte(parts[2]), []byte(expectedSig)) {
        return nil, fmt.Errorf("invalid signature")
    }

    // claims を解析
    claimsJSON, err := base64Decode(parts[1])
    if err != nil {
        return nil, err
    }

    var claims JWTClaims
    if err := json.Unmarshal(claimsJSON, &claims); err != nil {
        return nil, err
    }

    // 有効期限の確認
    if time.Now().Unix() > claims.ExpiresAt {
        return nil, fmt.Errorf("token expired")
    }

    return &claims, nil
}

func main() {
    secret := "my-secret-key"

    // Token を生成
    claims := JWTClaims{
        UserID:   1,
        Role:     "admin",
        ExpiresAt: time.Now().Add(1 * time.Hour).Unix(),
    }

    token, _ := createJWT(claims, secret)
    fmt.Printf("JWT: %s\n", token)

    // Token を検証
    validated, err := validateJWT(token, secret)
    if err != nil {
        fmt.Printf("検証に失敗しました: %v\n", err)
    } else {
        fmt.Printf("認証に成功しました: user=%d, role=%s\n", validated.UserID, validated.Role)
    }
}
▶ 試してみよう
💡 ヒント: 本番環境では、手動で実装するのではなく、成熟したJWTライブラリ (github.com/golang-jwt/jwt/v5) を使用してください。上記のコードは、JWTの仕組みを説明することのみを目的としています。サードパーティ製のライブラリは、トークンの種類、鍵のローテーション、標準クレームの検証など、より多くのエッジケースに対応しています。


4. アクセストークン + リフレッシュトークン

▶ サンプル:2要素認証

GO
package main

import (
    "crypto/rand"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "net/http"
    "sync"
    "time"
)

// ---------- Token サービス ----------

type TokenService struct {
    secret     string
    refreshTTL time.Duration
    // refresh token を保存(本番環境は Redis)
    refreshTokens map[string]int // token → userID
    mu            sync.RWMutex
}

func NewTokenService(secret string) *TokenService {
    return &TokenService{
        secret:         secret,
        refreshTTL:     7 * 24 * time.Hour,
        refreshTokens:  make(map[string]int),
    }
}

func (s *TokenService) GenerateTokens(userID int, role string) (accessToken, refreshToken string, err error) {
    // Access Token(15 分で期限切れ)
    accessClaims := JWTClaims{
        UserID:    userID,
        Role:      role,
        ExpiresAt: time.Now().Add(15 * time.Minute).Unix(),
    }
    accessToken, err = createJWT(accessClaims, s.secret)
    if err != nil {
        return "", "", err
    }

    // Refresh Token(ランダム文字列、7 日で期限切れ)
    bytes := make([]byte, 32)
    rand.Read(bytes)
    refreshToken = hex.EncodeToString(bytes)

    s.mu.Lock()
    s.refreshTokens[refreshToken] = userID
    s.mu.Unlock()

    return accessToken, refreshToken, nil
}

func (s *TokenService) RefreshAccessToken(refreshToken string) (string, error) {
    s.mu.RLock()
    userID, exists := s.refreshTokens[refreshToken]
    s.mu.RUnlock()

    if !exists {
        return "", fmt.Errorf("invalid refresh token")
    }

    // 新しい Access Token を生成
    claims := JWTClaims{
        UserID:    userID,
        Role:      "user",
        ExpiresAt: time.Now().Add(15 * time.Minute).Unix(),
    }
    return createJWT(claims, s.secret)
}

func (s *TokenService) RevokeRefreshToken(refreshToken string) {
    s.mu.Lock()
    delete(s.refreshTokens, refreshToken)
    s.mu.Unlock()
}

// ---------- Auth Handler ----------

type AuthHandler struct {
    tokenSvc *TokenService
}

type loginRequest struct {
    Username string `json:"username"`
    Password string `json:"password"`
}

type tokenResponse struct {
    AccessToken  string `json:"access_token"`
    RefreshToken string `json:"refresh_token"`
    TokenType    string `json:"token_type"`
    ExpiresIn    int64  `json:"expires_in"`
}

func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
    var req loginRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "invalid JSON")
        return
    }

    // ユーザー名とパスワードの検証(本番環境は bcrypt)
    if req.Username != "admin" || req.Password != "password" {
        writeError(w, http.StatusUnauthorized, "invalid credentials")
        return
    }

    accessToken, refreshToken, err := h.tokenSvc.GenerateTokens(1, "admin")
    if err != nil {
        writeError(w, http.StatusInternalServerError, "token generation failed")
        return
    }

    writeJSON(w, http.StatusOK, tokenResponse{
        AccessToken:  accessToken,
        RefreshToken: refreshToken,
        TokenType:    "Bearer",
        ExpiresIn:    900, // 15 分
    })
}

func (h *AuthHandler) Refresh(w http.ResponseWriter, r *http.Request) {
    var req struct {
        RefreshToken string `json:"refresh_token"`
    }
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "invalid JSON")
        return
    }

    accessToken, err := h.tokenSvc.RefreshAccessToken(req.RefreshToken)
    if err != nil {
        writeError(w, http.StatusUnauthorized, "invalid refresh token")
        return
    }

    writeJSON(w, http.StatusOK, map[string]string{"access_token": accessToken})
}
▶ 試してみよう
100%
sequenceDiagram
    participant Client
    participant API as API Gateway
    participant Auth as Auth Service

    Client->>API: POST /login (username, password)
    API->>Auth: 認証情報の確認
    Auth-->>API: access_token (15min) + refresh_token (7d)
    API-->>Client: 2つのトークンに戻る

    Client->>API: GET /orders (Bearer access_token)
    API->>API: 検証 access_token
    API-->>Client: 200 OK

    Client->>API: GET /orders (access_token 期限切れ)
    API-->>Client: 401 Unauthorized

    Client->>API: POST /refresh (refresh_token)
    API->>Auth: 検証 refresh_token
    Auth-->>API: 新しい access_token
    API-->>Client: トップへ戻る access_token

5. RBAC 権限ミドルウェア

▶ サンプル:RBACの実装

GO
パッケージ main

import (
    "context"
    "encoding/json"
    "net/http"
)

type UserRole 文字列

const (
    RoleAdmin UserRole = "admin"
    RoleUser  UserRole = "ユーザー"
    RoleGuest UserRole = "guest"
)

// 権限の定義
var rolePermissions = map[UserRole][]文字列{
    RoleAdmin: {"read:users", "write:users", "削除:users", "read:orders", "write:orders"},
    RoleUser:  {"read:orders", "write:orders"},
    RoleGuest: {"read:products"},
}

type Claims struct {
    UserID int
    Role   UserRole
}

// Context から認証情報を取得
func GetClaims(r *http.Request) *Claims {
    claims, _ := r.Context().Value("claims").(*Claims)
    return claims
}

// RBAC ミドルウェア
func RequirePermission(permission 文字列) Middleware {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            claims := GetClaims(r)
            if claims == nil {
                writeError(w, http.StatusUnauthorized, "not authenticated")
                return
            }

            permissions, exists := rolePermissions[claims.Role]
            if !exists {
                writeError(w, http.StatusForbidden, "no permissions defined for role")
                return
            }

            // 権限の確認
            hasPermission := false
            for _, p := range permissions {
                if p == permission {
                    hasPermission = true
                    break
                }
            }

            if !hasPermission {
                writeError(w, http.StatusForbidden, "insufficient permissions")
                return
            }

            next.ServeHTTP(w, r)
        })
    }
}

// ---------- ルーティング登録 ----------

func setupRoutes() http.Handler {
    mux := http.NewServeMux()

    // 公開エンドポイント
    mux.HandleFunc("POST /login", loginHandler)

    // 認証が必要なエンドポイント
    mux.Handle("GET /api/orders", RequirePermission("read:orders")(authMiddleware(http.HandlerFunc(listOrders))))
    mux.Handle("POST /api/orders", RequirePermission("write:orders")(authMiddleware(http.HandlerFunc(createOrder))))
    mux.Handle("DELETE /api/users/{id}", RequirePermission("削除:users")(authMiddleware(http.HandlerFunc(deleteUser))))

    return mux
}
▶ 試してみよう

(1) ミドルウェア・パイプライン

TEXT
Request → Auth Middleware → RBAC Middleware → Handler
              |                   |
         検証 JWT Token     確認 Role 権限
         注入 Claims       ユーザーには権限があります?
レベル ミドルウェア 担当業務
1 リカバリ パニックをキャッチし、500を返す
2 ロギング ロギングリクエストの所要時間
3 CORS クロスオリジンリクエストの処理
4 認証 JWTの検証、ユーザー情報の挿入
5 RBAC ロールの権限を確認
6 レート制限 トークンバケット方式によるスロットリング
7 ハンドラー ビジネスロジック

6. トークンバケット方式によるスループット制御

▶ サンプル:トークンバケットの実装

GO
package main

import (
    "net/http"
    "sync"
    "time"
)

type TokenBucket struct {
    mu         sync.Mutex
    tokens     float64
    maxTokens  float64
    refillRate float64
    lastRefill time.Time
}

func NewTokenBucket(rate float64, burst int) *TokenBucket {
    return &TokenBucket{
        tokens:     float64(burst),
        maxTokens:  float64(burst),
        refillRate: rate,
        lastRefill: time.Now(),
    }
}

func (tb *TokenBucket) Allow() bool {
    tb.mu.Lock()
    defer tb.mu.Unlock()

    // Refill tokens
    now := time.Now()
    elapsed := now.Sub(tb.lastRefill).Seconds()
    tb.tokens = min(tb.tokens+elapsed*tb.refillRate, tb.maxTokens)
    tb.lastRefill = now

    // Take a token
    if tb.tokens >= 1 {
        tb.tokens--
        return true
    }
    return false
}

func min(a, b float64) float64 {
    if a < b {
        return a
    }
    return b
}

// IP レートリミッター
type IPRateLimiter struct {
    mu       sync.RWMutex
    buckets  map[string]*TokenBucket
    rate     float64
    burst    int
}

func NewIPRateLimiter(rate float64, burst int) *IPRateLimiter {
    return &IPRateLimiter{
        buckets: make(map[string]*TokenBucket),
        rate:    rate,
        burst:   burst,
    }
}

func (rl *IPRateLimiter) GetBucket(ip string) *TokenBucket {
    rl.mu.Lock()
    defer rl.mu.Unlock()

    bucket, exists := rl.buckets[ip]
    if !exists {
        bucket = NewTokenBucket(rl.rate, rl.burst)
        rl.buckets[ip] = bucket
    }
    return bucket
}

// RateLimiterMiddleware
func RateLimiterMiddleware(rate float64, burst int) Middleware {
    limiter := NewIPRateLimiter(rate, burst)

    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            ip := r.RemoteAddr
            bucket := limiter.GetBucket(ip)

            if !bucket.Allow() {
                w.Header().Set("Retry-After", "1")
                writeError(w, http.StatusTooManyRequests, "rate limit exceeded")
                return
            }

            next.ServeHTTP(w, r)
        })
    }
}
▶ 試してみよう

7. 完全な例:API Gateway ミドルウェア・パイプライン

GO
// api_gateway.go
パッケージ main

import (
    "context"
    "encoding/json"
    "log"
    "net/http"
    "os"
    "os/signal"
    "strings"
    "syscall"
    "time"
)

// ---------- 型定義 ----------

type Middleware func(http.Handler) http.Handler

type Claims struct {
    UserID int
    Role   文字列
}

// ---------- ミドルウェア ----------

// Recovery
func Recovery(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("[PANIC] %v", err)
                writeError(w, http.StatusInternalServerError, "internal エラー")
            }
        }()
        next.ServeHTTP(w, r)
    })
}

// Logging
func Logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        log.Printf("[%s] %s %s", r.Method, r.URL.Path, r.RemoteAddr)
        next.ServeHTTP(w, r)
        log.Printf("[%s] %s → %v", r.Method, r.URL.Path, time.Since(start))
    })
}

// CORS
func CORS(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Access-Control-Allow-Origin", "*")
        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
        if r.Method == http.MethodOptions {
            w.WriteHeader(http.StatusNoContent)
            return
        }
        next.ServeHTTP(w, r)
    })
}

// Auth(JWT 認証)
func Auth(secret 文字列) Middleware {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            authHeader := r.Header.Get("Authorization")
            if !strings.HasPrefix(authHeader, "Bearer ") {
                writeError(w, http.StatusUnauthorized, "missing token")
                return
            }

            token := authHeader[7:]
            claims, err := validateJWT(token, secret)
            if err != nil {
                writeError(w, http.StatusUnauthorized, err.Error())
                return
            }

            ctx := context.WithValue(r.Context(), "claims", &Claims{
                UserID: claims.UserID,
                Role:   claims.Role,
            })
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

// RBAC
func RequireRole(roles ...文字列) Middleware {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            claims, ok := r.Context().Value("claims").(*Claims)
            if !ok {
                writeError(w, http.StatusUnauthorized, "not authenticated")
                return
            }

            for _, role := range roles {
                if claims.Role == role {
                    next.ServeHTTP(w, r)
                    return
                }
            }

            writeError(w, http.StatusForbidden, "insufficient permissions")
        })
    }
}

// Rate Limiter
var rateLimiter = NewIPRateLimiter(10, 20) // 10 req/s, burst 20

func RateLimit(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ip := r.RemoteAddr
        bucket := rateLimiter.GetBucket(ip)
        if !bucket.Allow() {
            writeError(w, http.StatusTooManyRequests, "rate limit exceeded")
            return
        }
        next.ServeHTTP(w, r)
    })
}

// ---------- Chain ----------

func Chain(h http.Handler, mws ...Middleware) http.Handler {
    for i := len(mws) - 1; i >= 0; i-- {
        h = mws[i](h)
    }
    return h
}

// ---------- Handlers ----------

type OrderHandler struct{}

func (h *OrderHandler) ListOrders(w http.ResponseWriter, r *http.Request) {
    writeJSON(w, http.StatusOK, map[文字列]文字列{"orders": "list"})
}

func (h *OrderHandler) CreateOrder(w http.ResponseWriter, r *http.Request) {
    writeJSON(w, http.StatusCreated, map[文字列]文字列{"順序": "created"})
}

type AdminHandler struct{}

func (h *AdminHandler) DeleteUser(w http.ResponseWriter, r *http.Request) {
    writeJSON(w, http.StatusOK, map[文字列]文字列{"deleted": "ユーザー"})
}

// ---------- ユーティリティ関数 ----------

func writeJSON(w http.ResponseWriter, ステータス int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(ステータス)
    json.NewEncoder(w).Encode(data)
}

func writeError(w http.ResponseWriter, ステータス int, msg 文字列) {
    writeJSON(w, ステータス, map[文字列]文字列{"エラー": msg})
}

// ---------- Main ----------

func main() {
    jwtSecret := "super-secret-key"
    orders := &OrderHandler{}

    mux := http.NewServeMux()

    // 公開エンドポイント
    mux.HandleFunc("POST /login", func(w http.ResponseWriter, r *http.Request) {
        writeJSON(w, http.StatusOK, map[文字列]文字列{"token": "login-implementation"})
    })

    // 認証 + レート制限が適用されるエンドポイント
    mux.Handle("GET /api/orders", Chain(
        http.HandlerFunc(orders.ListOrders),
        Auth(jwtSecret),
        RateLimit,
    ))

    mux.Handle("POST /api/orders", Chain(
        http.HandlerFunc(orders.CreateOrder),
        Auth(jwtSecret),
        RateLimit,
    ))

    // 管理者用エンドポイント(Auth + RBAC + RateLimit)
    mux.Handle("DELETE /api/users/{id}", Chain(
        http.HandlerFunc((&AdminHandler{}).DeleteUser),
        Auth(jwtSecret),
        RequireRole("admin"),
        RateLimit,
    ))

    // グローバルミドルウェア(Recovery → Logging → CORS → ルート)
    app := Chain(mux, Recovery, Logging, CORS)

    サーバー := &http.Server{Addr: ":8080", Handler: app}

    go func() {
        sigCh := make(chan os.Signal, 1)
        signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
        <-sigCh
        log.Println("閉じている...")
        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
        defer cancel()
        サーバー.Shutdown(ctx)
    }()

    log.Println("API ゲートウェイの起動は :8080")
    if err := サーバー.ListenAndServe(); err != http.ErrServerClosed {
        log.Fatal(err)
    }
}
🔥 よくある間違い: ミドルウェアの順序は極めて重要です。 リカバリーは最外層に配置する必要があります(すべてのパニックを捕捉するため)。AuthとRBACはRateLimitの前に配置する必要があります。レート制限は未認証のリクエストにも適用されるべきですが、未認証のユーザーが最初にトークンを消費することは許されてはなりません。一般的な順序:Recovery → Logging → CORS → Auth → RBAC → RateLimit → Handler。


❓ よくある質問

Q JWTはどのように実装されますか?
A JWTは、ヘッダー(アルゴリズム+タイプ)、ペイロード(クレーム)、署名の3つの部分で構成されます。Goではgithub.com/golang-jwt/jwt/v5の使用が推奨されています。アクセストークンの有効期間は短く(15分)、リフレッシュトークンの有効期間は長く(7日間)、サーバー上に保存されます。
Q アクセストークンとリフレッシュトークンの違いは何ですか?
A アクセストークンはAPI認証に使用されます。有効期間は短く(15分間)、ユーザー情報と権限が含まれています。リフレッシュトークンは新しいアクセストークンを取得するために使用されます。有効期間は長く(7日間)、サーバー上に保存されます。アクセストークンの有効期限が切れた場合、クライアントはリフレッシュトークンを使用して、ユーザーに再ログインを求めずに自動的に新しいアクセストークンを取得します。
Q RBACはどのように実装されていますか?
A RBAC(ロールベースのアクセス制御)では、各ロールに一連の権限が割り当てられ、ミドルウェアが現在のユーザーのロールに、そのリクエストに必要な権限があるかどうかを確認します。実装:rolePermissions[role] = []permission + ミドルウェアによる確認 permission in rolePermissions[claims.Role]
Q どのレート制限アルゴリズムを選べばよいですか?
A 最も一般的に使用されているのはトークンバケットです。これはバーストを許容しつつ、平均レートを制御することができます。実装方法:1秒あたりのレートトークンをバケットに補充し、バースト制限を設定します。各リクエストで1つのトークンが消費され、トークンが不足している場合は429エラーが返されます。APIのレート制限に適しています。
Q リカバリミドルウェアがパニックメッセージを抑制しないようにするにはどうすればよいですか?
A リカバリ処理内でスタックトレース全体をログに記録し、カスタムエラー型を使用してそれを返します。debug.Stack() はスタックトレースを出力します。ログレベルは ERROR を使用してください。本番環境では、スタックトレースをクライアントに返さず、サーバー上でのみログに記録してください。
Q ミドルウェアパイプラインの実行順序はどのようなものですか?
A オニオンモデルです。Chain(h, A, B, C) → リクエストは A → B → C → Handler → C → B → A の順を通過します。登録順序は、最外層から最内層へと進みます。Recovery は最外層にあり(どの層から投げられたパニックも捕捉します)、Handler は最内層にあります(ビジネスロジック)。
Q JWTはどのように失効させるのですか?
A JWTはステートレスであるため、一度発行されると有効期限が切れるまで失効させることはできません。解決策:(1) 短期トークン+リフレッシュトークン(リフレッシュトークンは失効させることができます);(2) ブラックリスト(失効したJWT IDをRedisに保存); (3) バージョン番号(ユーザーがパスワードを変更した際にバージョン番号をインクリメントし、古いトークンを無効にする)。

📖 まとめ


📝 練習問題

  1. 基本演習(難易度 ⭐):簡単な認証ミドルウェアを実装してください。/api/protectedというエンドポイントを作成し、リクエストヘッダーにAuthorization: Bearer token123が含まれていることを条件とします。トークンが有効な場合は{"message": "access granted"}を返し、そうでない場合は401エラーを返してください。

  2. 上級問題(難易度 ⭐⭐)完全なJWT認証システムを実装してください。要件:(1) 登録用の POST /register(パスワードはbcryptで暗号化);(2) ログイン用の POST /login(access_token + refresh_token を返す); (3) すべての /api/* エンドポイントにおいて access_token を検証する Auth ミドルウェア;(4) refresh_token を新しい access_token と交換するための POST /refresh;(5) refresh_token を失効させるための POST /logout

  3. 課題(難易度:⭐⭐⭐)RBACとレート制限を備えたブログ管理APIを実装してください。要件:(1) 3つのロール:admin、editor、reader;(2) admin:すべての権限;(3) editor:自身の投稿の作成、編集、削除;(4) reader:読み取り専用アクセス; (5) ロールごとに異なるレート制限を設定すること;(6) すべてのエンドポイントは、以下のミドルウェアパイプラインを経由すること:Recovery → Logging → Auth → RBAC → RateLimit → Handler;(7) -race を使用して、並行処理の安全性を検証すること。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%