ミドルウェアは、Echo で横断的関心事(cross-cutting concerns)——ログ・認証・CORS・レート制限・セキュリティヘッダ——を、ハンドラ本体を汚さずに差し込む仕組みです。ここを雑に積むと、CORS が効かない・CSRF で正規リクエストまで弾く・panic でプロセスが落ちる・認証より先に重い処理が走るといった本番事故に直結します。
この記事は、Go Echo 本番運用ガイドのミドルウェア編です。仕組み → 並び順 → 個別の設定の順に、Echo v5 の主要ミドルウェアを「いつ・どう設定するか」まで踏み込んで解説します。
この記事のルール:API は Echo 公式ドキュメント(v5・2026年6月時点) に基づきます。v5 はミドルウェアの署名が
*echo.Contextに変わり、middleware.Logger()とmiddleware.Timeout()がコアから削除され、CORSが可変長引数化、KeyAuthの Validator 署名が変更されるなど、v4 から差分が大きい領域です。本番投入前に必ず公式で最新を確認してください。シークレット(JWT 署名鍵など)は環境変数前提です。
1. ミドルウェアの仕組み:玉ねぎの皮を理解する
Echo のミドルウェアは、ハンドラを受け取ってハンドラを返す関数です。
type MiddlewareFunc func(next echo.HandlerFunc) echo.HandlerFunc
next(c) を呼ぶ前がリクエスト前処理、後がレスポンス後処理になります。
func timing(next echo.HandlerFunc) echo.HandlerFunc {
return func(c *echo.Context) error {
start := time.Now() // ── 前処理
err := next(c) // ── 次のミドルウェア/ハンドラへ
// ── 後処理
c.Logger().Info("handled", "path", c.Request().URL.Path, "took", time.Since(start))
return err // エラーは必ず伝播させる(握りつぶさない)
}
}
登録した順に外側から内側へ積まれます。e.Use(A); e.Use(B) なら A → B → ハンドラ → B → A の順で制御が流れます。この「玉ねぎ構造」が並び順の重要性の正体です。
1.1 3つの適用レベル
// ① ルート全体(最外周)— 全リクエストに効く
e.Use(middleware.Recover())
// ② グループ単位 — そのプレフィックス配下だけ
api := e.Group("/api", middleware.CORS("https://app.example.com"))
// ③ 個別ルート — その1本だけ
e.GET("/admin/report", report, requireAdmin)
権限境界(公開/認証必須/管理者専用)はグループで表現するのが定石です。「この API 群には必ず認証が掛かる」をコードの構造で保証できます。
1.2 Skipper:特定リクエストだけ素通しする
ほぼ全てのミドルウェアは Skipper func(c *echo.Context) bool を持ち、true を返すとそのミドルウェアを飛ばします。ヘルスチェックやメトリクス収集を除外する定番パターンです。
e.Use(middleware.GzipWithConfig(middleware.GzipConfig{
Skipper: func(c *echo.Context) bool {
return strings.Contains(c.Path(), "metrics") // /metrics は圧縮しない
},
}))
各ミドルウェアには XxxWithConfig(XxxConfig{...}) という設定付きコンストラクタがあり、引数なしの Xxx() はデフォルト設定の糖衣です。
2. 並び順:本番の推奨スタック
結論から。本番 API のルート全体には、おおむね次の順で積みます。順序には理由があります。
e := echo.New()
e.Use(middleware.Recover()) // ① panic を最外周で回収(最初=最も外)
e.Use(middleware.RequestID()) // ② 相関 ID を採番(以降のログに載る)
e.Use(middleware.RequestLogger()) // ③ アクセスログ(slog)
e.Use(middleware.Secure()) // ④ セキュリティヘッダ
e.Use(middleware.CORS(allowedOrigins...)) // ⑤ CORS(プリフライトを早く返す)
e.Use(middleware.RateLimiter(store)) // ⑥ レート制限(重い処理の前で弾く)
e.Use(middleware.BodyLimit(2_097_152)) // ⑦ 本文サイズ上限
e.Use(middleware.Gzip()) // ⑧ レスポンス圧縮(最も内側に近い)
// 認証は「全体」ではなくグループに掛けることが多い(公開ルートを除外するため)
| 順 | ミドルウェア | なぜこの位置か |
|---|---|---|
| ① | Recover | どこで panic しても回収するには最外周が必須 |
| ② | RequestID | 後続の全ログ・エラーに相関 ID を載せるため早めに |
| ③ | RequestLogger | リクエストの最初から最後までの時間を測るため外側に |
| ④ | Secure | レスポンスヘッダはエラー応答にも付けたい |
| ⑤ | CORS | プリフライト(OPTIONS)を認証や重い処理の前で返す |
| ⑥ | RateLimiter | 攻撃・過剰アクセスを重い処理の前で遮断する |
| ⑦ | BodyLimit | 巨大ボディを読み込む前にサイズで弾く(DoS 緩和) |
| ⑧ | Gzip | 圧縮は最後に書き出すレスポンスに掛かればよい |
よくある事故:認証ミドルウェアを
RateLimiterより外側に置くと、未認証の攻撃者が認証処理(DB 照会・ハッシュ計算)を踏ませ放題になります。**「安く弾けるものほど外側で弾く」**が原則です。
3. Recover:panic でプロセスを殺さない
Go では goroutine 内の未回収 panic はプロセス全体を落とします。Recover はチェーンのどこで panic しても回収し、スタックトレースを出して制御を集中エラーハンドラへ渡します。本番では必須、かつ最外周に置きます。
e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{
StackSize: 4 << 10, // 4KB(デフォルト)
DisableStackAll: false, // 他 goroutine のスタックも含める
DisablePrintStack: false, // スタックトレースを出力する
}))
StackSize はトレースを取り込むバッファ長です。panic 時の調査効率に直結するため、本番では出力を切らないことを推奨します。
4. RequestLogger:slog で構造化アクセスログ
v5 はロガーを標準の log/slog に統一しました。middleware.RequestLogger() はデフォルトの slog ロガーへ出力しますが、本番では LogValuesFunc で出したい項目を JSON で明示します。
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
LogStatus: true,
LogURI: true,
LogError: true,
LogLatency: true,
HandleError: true, // エラーを集中ハンドラへ正しく伝播
LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error {
attrs := []slog.Attr{
slog.String("uri", v.URI),
slog.Int("status", v.Status),
slog.Duration("latency", v.Latency),
}
if v.Error == nil {
logger.LogAttrs(context.Background(), slog.LevelInfo, "REQUEST", attrs...)
} else {
logger.LogAttrs(context.Background(), slog.LevelError, "REQUEST_ERROR",
append(attrs, slog.String("err", v.Error.Error()))...)
}
return nil
},
}))
RequestLoggerValues は Status/URI/Method/Latency/Error/RemoteIP などを持ちます。HandleError: true を忘れると、エラーが集中ハンドラに渡る前にログ側で消費され、ステータスがズレることがあります。middleware.RequestID() と組み合わせ、request_id を全行に載せれば 1 リクエストを貫通して追跡できます(可観測性の実践)。
5. CORS:v5 で引数が変わった最頻出の事故ポイント
ブラウザから別オリジンの API を叩くなら CORS は必須です。v5 では middleware.CORS() が許可オリジンを可変長引数で受ける形に変わりました。
// 簡易:許可するオリジンを列挙
e.Use(middleware.CORS("https://app.example.com", "https://admin.example.com"))
// 詳細設定
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"https://labstack.com", "https://labstack.net"},
AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept},
AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete},
MaxAge: 3600, // プリフライト結果を1時間キャッシュ
}))
| フィールド | 意味 | 既定 |
|---|---|---|
AllowOrigins | Access-Control-Allow-Origin に出すオリジン(必須) | — |
AllowMethods | 許可メソッド | GET/HEAD/PUT/PATCH/POST/DELETE |
AllowHeaders | 許可リクエストヘッダ | 空 |
AllowCredentials | Cookie/認証情報を伴うリクエストを許すか | false |
MaxAge | プリフライトのキャッシュ秒数 | 0(ヘッダ非送出) |
セキュリティ警告(重要):ワイルドカード
"*"とAllowCredentials: trueの同時指定は脆弱です。これは「あらゆるサイトが Cookie 付きで API を叩ける」状態を意味します。Echo はこの危険な組み合わせを意図的に panic で拒否します。認証情報を伴うなら、信頼できるオリジンを明示列挙してください。
6. Secure:セキュリティヘッダを一括付与
XSS・クリックジャッキング・MIME スニッフィング対策のレスポンスヘッダをまとめて付けます。
e.Use(middleware.SecureWithConfig(middleware.SecureConfig{
XSSProtection: "1; mode=block", // X-XSS-Protection
ContentTypeNosniff: "nosniff", // X-Content-Type-Options
XFrameOptions: "SAMEORIGIN", // X-Frame-Options(クリックジャッキング対策)
HSTSMaxAge: 31536000, // Strict-Transport-Security(1年)
HSTSPreloadEnabled: true,
ContentSecurityPolicy: "default-src 'self'", // CSP
}))
XSSProtection/ContentTypeNosniff/XFrameOptions はデフォルトでも安全側の値が入りますが、**HSTSMaxAge と ContentSecurityPolicy はデフォルトが無効(0 / 空)**なので、HTTPS 本番では明示設定します。空文字を渡すとその保護は無効化される点に注意してください。CSP の設計はアプリ依存が大きいため、まず default-src 'self' から段階的に緩める運用が安全です。
7. CSRF:Cookie セッション型アプリの必須対策
CSRF は、Cookie ベースのセッション認証を使うアプリで必要です(トークンを Authorization ヘッダで送る純粋な API では通常不要)。
e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
TokenLookup: "header:X-CSRF-Token", // どこからトークンを探すか
ContextKey: "csrf", // c.Get("csrf") で取得
CookieName: "_csrf",
CookiePath: "/",
CookieSecure: true, // HTTPS のみ送出
CookieHTTPOnly: true, // JS から読めない
CookieSameSite: http.SameSiteStrictMode,
CookieMaxAge: 86400, // 24時間
}))
サーバー側では、生成されたトークンを c.Get("csrf") で取り出し、HTML テンプレートに埋め込んでクライアントへ渡します。クライアントは次回リクエストでそれを X-CSRF-Token ヘッダ(または設定した TokenLookup)に載せて送り返します。SPA なら CSRF Cookie を読んでヘッダに付け替える方式(double-submit cookie)が一般的です。
8. 認証:KeyAuth(API キー)と JWT(echo-jwt)
8.1 KeyAuth:API キー / Bearer トークン検証
KeyAuth は、ヘッダ・クエリ・Cookie から鍵を取り出して検証します。v5 で Validator の署名が変わり、抽出元(ExtractorSource)も渡されるようになりました。
e.Use(middleware.KeyAuthWithConfig(middleware.KeyAuthConfig{
KeyLookup: "header:Authorization:Bearer ", // "Bearer " を剥がして鍵を取得
Validator: func(c *echo.Context, key string, source middleware.ExtractorSource) (bool, error) {
// 定数時間比較で鍵を検証(タイミング攻撃対策)
valid, err := apiKeys.Verify(c.Request().Context(), key)
return valid, err
},
ErrorHandler: func(c *echo.Context, err error) error {
return echo.NewHTTPError(http.StatusUnauthorized, "invalid api key")
},
}))
KeyLookup は "<source>:<name>" 形式で、"header:Authorization:Bearer " のようにプレフィックスを剥がす指定もできます。
8.2 JWT:コア外の echo-jwt/v5 モジュール
v5 でも JWT ミドルウェアは Echo コアに含まれず、別モジュールです。import が v5 系であることに注意してください。
go get github.com/labstack/echo-jwt/v5
import echojwt "github.com/labstack/echo-jwt/v5"
// 簡易:署名鍵だけ(鍵は必ず環境変数から)
e.Use(echojwt.JWT([]byte(os.Getenv("JWT_SIGNING_KEY"))))
// 詳細設定
e.Use(echojwt.WithConfig(echojwt.Config{
SigningKey: []byte(os.Getenv("JWT_SIGNING_KEY")),
SigningMethod: "HS256", // デフォルト HS256
TokenLookup: "header:Authorization:Bearer ",
ContextKey: "user", // 検証済みクレームの格納先(デフォルト "user")
ErrorHandler: func(c *echo.Context, err error) error {
return echo.NewHTTPError(http.StatusUnauthorized, "invalid or expired token")
},
}))
検証済みのクレームは c.Get("user") で取り出します。公開ルート(ログイン・ヘルスチェック)を巻き込まないよう、JWT は認証必須グループにだけ掛けるのが定石です。
api := e.Group("/api/v1")
{
api.POST("/login", login) // 認証不要
}
secured := api.Group("")
secured.Use(echojwt.JWT([]byte(os.Getenv("JWT_SIGNING_KEY"))))
{
secured.GET("/me", me) // 認証必須
}
非対称鍵(RS256)での検証や JWKS、Cognito 等の IdP 連携を本格運用するなら、署名検証の落とし穴はJWT RS256 検証の記事が参考になります。HS256 の共有鍵は漏洩=なりすましに直結するため、必ずシークレットマネージャ/環境変数で管理します。
9. RateLimiter:重い処理の前で過剰アクセスを遮断
レート制限は、ブルートフォースや過剰アクセスを安いコストで早期に弾くための層です。インメモリストアが標準で付属します(複数インスタンスでは Redis 等の共有ストアが必要)。
// 簡易:毎秒 20 リクエスト(rate が float のとき burst は rate の切り捨て)
e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20)))
本番では、誰をレート制限するか(IP/ユーザー)と、超過時の応答を明示します。
config := middleware.RateLimiterConfig{
Store: middleware.NewRateLimiterMemoryStoreWithConfig(
middleware.RateLimiterMemoryStoreConfig{
Rate: 10, // 毎秒 10
Burst: 30, // バースト許容
ExpiresIn: 3 * time.Minute, // 識別子エントリの保持時間
},
),
IdentifierExtractor: func(c *echo.Context) (string, error) {
return c.RealIP(), nil // プロキシ背後では RealIP を使う
},
ErrorHandler: func(c *echo.Context, err error) error {
// 識別子抽出に失敗(通常は起きない)
return echo.NewHTTPError(http.StatusForbidden)
},
DenyHandler: func(c *echo.Context, identifier string, err error) error {
// 制限超過時。429 を返す
return c.JSON(http.StatusTooManyRequests, map[string]string{"error": "rate limit exceeded"})
},
}
e.Use(middleware.RateLimiterWithConfig(config))
| フィールド | 役割 |
|---|---|
Store | レート制限のロジック(メモリ/自前の RateLimiterStore 実装) |
IdentifierExtractor | 誰を制限するかの識別子(c.RealIP()、認証済みユーザー ID 等) |
DenyHandler | 制限超過時の応答(429 を返すのが定石) |
ErrorHandler | 識別子抽出に失敗したときの応答 |
Skipper | 特定リクエストを除外 |
c.RealIP()はX-Forwarded-For/X-Real-IPを見ます。信頼できるプロキシ(ALB・Cloudflare 等)の背後でのみ正しく機能し、それ以外ではクライアントが偽装できる点に注意。Echo.IPExtractorで信頼できる前段の設定を行ってください。複数インスタンス構成ではインメモリストアはインスタンスごとに独立するため、Redis ベースの共有ストアに差し替えます(サーバーレスのレート制限の考え方も同じ罠を扱っています)。
10. その他の定番:BodyLimit / Gzip / RequestID / Static
// BodyLimit:本文サイズ上限。v5 は「バイト数(整数)」で指定(v4 の "2M" 文字列から変更)
e.Use(middleware.BodyLimit(2_097_152)) // 2MB を超えると 413
// Gzip:レスポンス圧縮
e.Use(middleware.GzipWithConfig(middleware.GzipConfig{
Level: 5, // 圧縮レベル(デフォルト -1)
MinLength: 1024, // これ未満のレスポンスは圧縮しない
}))
// RequestID:相関 ID を採番(無ければ生成)。RequestLogger と必ずセットで
e.Use(middleware.RequestID())
// Static:静的ファイル配信
e.Static("/static", "web/assets") // /static/* → web/assets/ 以下
BodyLimit の整数バイト指定は v5 の変更点です。ネット上の "2M" という文字列指定は v4 の書き方なので、コピペ時に注意してください。
11. カスタムミドルウェア:横断的関心事を自作する
内蔵で足りない要件(テナント解決・監査ログ・APM 連携)は、自作します。next(c) の戻り値を必ず伝播し、Skipper 相当の早期 return を用意するのが作法です。
// テナント境界をヘッダから解決し、Context に詰めるミドルウェア
func TenantResolver(repo TenantRepository) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c *echo.Context) error {
tenantID := c.Request().Header.Get("X-Tenant-ID")
if tenantID == "" {
return echo.NewHTTPError(http.StatusBadRequest, "missing tenant")
}
tenant, err := repo.Find(c.Request().Context(), tenantID)
if err != nil {
return echo.ErrForbidden // 存在しない/権限なし
}
c.Set("tenant", tenant) // ハンドラ・後続ミドルウェアへ受け渡す
return next(c) // ← 戻り値を握りつぶさない
}
}
}
// グループに適用
api := e.Group("/api/v1")
api.Use(TenantResolver(tenantRepo))
依存(TenantRepository 等)を引数で受けるクロージャ方式にすると、テスト時にモックを差し込めます。これはクリーンアーキテクチャ + DIと同じ思想で、ミドルウェアもユニットテスト可能に保てます。
まとめ:ミドルウェア設計の7原則
Recoverは最外周・最初。panic でプロセスを殺さない。- 安く弾けるものほど外側で。
RateLimiter・BodyLimitは重い処理・認証の前に。 - CORS は v5 で可変長引数。
"*"×AllowCredentials:trueは Echo が panic で拒否する危険な組み合わせ。 Secureの HSTS と CSP はデフォルト無効。HTTPS 本番では明示する。- JWT は
echo-jwt/v5(コア外)、KeyAuth の Validator 署名は v5 で変更。鍵は環境変数。 - 認証はグループに掛ける。公開ルートを巻き込まない。
- カスタムミドルウェアは
next(c)を必ず伝播し、依存はクロージャで注入してテスト可能に。
ミドルウェアは「積めば効く」ものではなく、順序と設定で挙動が決まる層です。本記事のスタックを起点に、自分のアプリの認証方式(Cookie か Bearer か)とインフラ(単一か複数インスタンスか)に合わせて調整してください。次は、ミドルウェアを抜けた先の入力検証とエラー整形——バインディング&バリデーション&エラー設計へ。全体像はGo Echo 本番運用ガイドに戻って確認できます。