GinでCookieの取得・検証はhandlerとmiddlewareのどちらで行うべきか
はじめに
Go + Ginでアプリを作っていると、「このCookieのチェック、handlerに書くべきかmiddlewareに書くべきか」と迷うことがあります。
たとえば提携先サービスとの連携で使う partner_code Cookie を考えると、次の確認が必要になります。
- Cookieが存在するかどうかの確認
- Cookieの値が規定のフォーマットかどうかの確認
- その提携コードが業務上許可された値かどうかの確認
ここで分けて考えたいのは、次の2つです。
- HTTP入力としての検証
Cookieがあるか、形式が正しいかを確認する話です。 - 業務上の妥当性確認
その値を業務的に信じてよいか、許可された値かを確認する話です。
これをすべてhandlerに書くと、HTTP入力の処理と業務上の判定が混ざります。逆に、何でもmiddlewareに寄せると単発のAPIでは重くなりがちです。
この記事では、「Cookieをどこで読むか」と「取り出した値を業務的にどう検証するか」を分けて整理します。そのうえで、handlerで処理する場合とmiddlewareで処理する場合をコードで見比べます。主題はCookie処理そのものより、handler / middleware / service の責務をどう分けるかです。
サンプルコード
基本的な考え方
先に結論を書くと、責務は次のように分けるのが自然です。
| レイヤー | やること |
|---|---|
| handler / middleware | Cookieを読む、未設定・形式不正を弾く(HTTP入力の話) |
| service / usecase | その値が業務上有効かどうかを判定する(業務ロジックの話) |
「Cookieがない」「フォーマットが壊れている」はHTTP入力の問題です。これはhandlerかmiddlewareで弾きます。
「このコードは許可された提携先か」はビジネスルールの話です。これはserviceで判断します。
middlewareはHTTPレイヤーで共通化しやすい処理に向いています。一方で、業務ルールまで持ち込むと責務が曖昧になりやすく、別のhandlerや別の経路で再利用しにくくなります。
この記事では便宜上 service と書きますが、設計によっては usecase や domain service、policy のような名前で置いても考え方は同じです。
大事なのは、serviceにCookie取得の責務を持ち込まないことです。serviceは partnerCode string を受け取るだけにして、gin.Context や *http.Cookie を知らない形にしておくと扱いやすくなります。
プロジェクト構成
gin-cookie-boundary-sample/
├── main.go
├── handler/
│ ├── partner.go # パターン1: handlerでCookie取得
│ └── partner_via_middleware.go # パターン2: middlewareから受け取る
├── middleware/
│ └── partner_cookie.go # パターン2のmiddleware
└── service/
└── partner.go # 両パターン共通のservice
パターン1: handlerでCookieを取得する
コード
// handler/partner.go
var partnerCodePattern = regexp.MustCompile(`^[a-zA-Z0-9\-]{3,32}$`)
func (h *PartnerHandler) GetPartnerInfo(c *gin.Context) {
// 1. Cookieを読む
partnerCode, err := c.Cookie("partner_code")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "partner_code cookieが設定されていません",
})
return
}
// 2. 形式チェック
if partnerCode == "" || !partnerCodePattern.MatchString(partnerCode) {
c.JSON(http.StatusBadRequest, gin.H{
"error": "partner_code cookieの形式が不正です",
})
return
}
// 3. serviceへ渡す(業務判定はここから先)
info, err := h.svc.GetPartnerInfo(partnerCode)
if err != nil {
c.JSON(http.StatusForbidden, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, info)
}
動作フロー
GET /handler/partner-info
→ handler: c.Cookie("partner_code") でCookieを取得
→ handler: フォーマットチェック
→ service: 業務的に有効な提携コードか判定
→ レスポンス
どういうときに向くか
- そのAPIにしかCookieが不要な場合
- 後から共通化する必要がないと分かっている場合
責務がhandlerに集まるぶん、流れを追いやすいのが利点です。
パターン2: middlewareでCookieを取得する
middlewareのコード
// middleware/partner_cookie.go
const PartnerCodeKey = "partnerCode"
var partnerCodePattern = regexp.MustCompile(`^[a-zA-Z0-9\-]{3,32}$`)
func RequirePartnerCookie() gin.HandlerFunc {
return func(c *gin.Context) {
// 1. Cookieを読む
partnerCode, err := c.Cookie("partner_code")
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "partner_code cookieが設定されていません",
})
return
}
// 2. 形式チェック
if partnerCode == "" || !partnerCodePattern.MatchString(partnerCode) {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "partner_code cookieの形式が不正です",
})
return
}
// 3. contextに詰めて次のhandlerへ
c.Set(PartnerCodeKey, partnerCode)
c.Next()
}
}
handlerのコード(middleware版)
// handler/partner_via_middleware.go
func (h *PartnerMiddlewareHandler) GetPartnerInfo(c *gin.Context) {
// middlewareがcontextに詰めた値を取り出すだけ
partnerCodeValue, exists := c.Get(middleware.PartnerCodeKey)
if !exists {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "partnerCodeがcontextにありません(middlewareの設定を確認してください)",
})
return
}
partnerCode, ok := partnerCodeValue.(string)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "partnerCodeの型が不正です",
})
return
}
info, err := h.svc.GetPartnerInfo(partnerCode)
if err != nil {
c.JSON(http.StatusForbidden, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, info)
}
ルーティング
// main.go
// グループにmiddlewareを適用するだけで、配下の全handlerにCookie検証が入る
middlewareGroup := r.Group("/middleware", middleware.RequirePartnerCookie())
{
middlewareGroup.GET("/partner-info", partnerMiddlewareHandler.GetPartnerInfo)
// 同じmiddlewareを使う別エンドポイントをここに追加できる
// middlewareGroup.GET("/other-endpoint", otherHandler.DoSomething)
}
動作フロー
GET /middleware/partner-info
→ middleware: c.Cookie("partner_code") でCookieを取得
→ middleware: フォーマットチェック
→ middleware: c.Set("partnerCode", partnerCode) でcontextに詰める
→ handler: c.Get("partnerCode") で取り出す
→ service: 業務的に有効な提携コードか判定
→ レスポンス
どういうときに向くか
- 同じCookieを複数のAPIで使う場合
- 認証・認可の仕組みとして共通化したい場合
複数のエンドポイントで同じCookieを扱うなら、handlerごとに同じ処理を書くよりmiddlewareにまとめた方が保守しやすくなります。ただし、そこで扱うのはHTTP入力として共通化しやすい範囲に留めた方が安全です。
middlewareに寄せすぎると困ること
middleware化は便利ですが、寄せすぎると扱いにくさも出てきます。
context経由の受け渡しが見えにくくなる
handlerの引数を見ても「何を受け取っているか」が分かりません。c.Get("partnerCode") という文字列キーの一致と、middlewareが先に実行されることに依存するためです。つまり、middlewareとhandlerの結合がコード上に明示されず、実行時まで崩れたことに気づきにくい構造になります。middlewareを外したり差し替えたりしてもコンパイルエラーにはならず、exists == false や型不一致で初めて気づく、という状況になりがちです。
テストにmiddlewareを前提とする必要が出る
handlerのテストを書くとき、middlewareが c.Set("partnerCode", ...) を済ませている前提で動くため、テスト側でその状態を事前に作る必要があります。handler単体のテストがやや煩雑になります。
「このAPIだけCookieが任意」という例外が扱いにくくなる
Cookieが全ルートで必須ならmiddlewareは有効ですが、「このエンドポイントだけCookieなしでも動く」という例外が出てきたとき、middleware側で条件分岐が増えてロジックが複雑になります。その場合はhandlerで個別に処理する方がシンプルです。
service層: 両パターン共通
// service/partner.go
// 許可された提携コードの一覧(実務ではDBや設定ファイルから取得する)
var validPartnerCodes = map[string]bool{
"partner-a": true,
"partner-b": true,
}
// GetPartnerInfo は提携コードを受け取り、業務的に有効かどうかを検証して情報を返す。
// Cookieや gin.Context には一切依存しない。
func (s *PartnerService) GetPartnerInfo(partnerCode string) (map[string]string, error) {
if !validPartnerCodes[partnerCode] {
return nil, errors.New("許可されていない提携コードです: " + partnerCode)
}
return map[string]string{
"partnerCode": partnerCode,
"status": "active",
"message": "提携先として有効です",
}, nil
}
serviceはstringを受け取るだけです。gin.Context も *http.Cookie も登場しません。
この形にしておくと、serviceのテストは純粋な関数呼び出しとして書けます。HTTPリクエストを組み立てる必要がなく、入出力の確認に集中できます。
Cookieはクライアント入力である
ここで一点、Cookieに固有の話をしておきます。
Cookieはクライアント(ブラウザや外部サービス)から送られてくる値です。ヘッダーやクエリパラメータと同様、サーバーから見れば「ユーザーが自由に操作できる入力」です。
そのため、扱い方には注意が必要です。
形式が正しくても信用しすぎない
形式チェック(正規表現など)はあくまで「壊れた入力」を弾くためのものです。フォーマットが正しい値でも、業務的に許可された値かどうかは必ずサーバー側で確認します。今回のサンプルでいえば、それがserviceの validPartnerCodes によるチェックです。
改ざんリスクを意識する
提携コードのような値がセキュリティ上重要な場合(例: これによってアクセス可能なリソースが変わる場合)は、ただの文字列Cookieは信頼の根拠として弱いです。JWTのような署名付きトークンやサーバー側セッションと組み合わせることで、改ざんを検出できます。
今回のサンプルは「提携先の導線識別」を想定しており、最終的な認可はservice内のホワイトリストで行っているため、Cookieが改ざんされても許可されていない値として弾かれます。ただし実務では、Cookieがどこまでの権限に影響するかに応じて設計を検討してください。
認証用Cookieと識別用Cookieの違い
middlewareに寄せるか、handlerで処理するかを決めるときは、そのCookieが何のためにあるのかを先に整理すると判断しやすくなります。
識別用Cookie(今回のサンプルの想定)
「どの提携導線から来たか」を識別するためのCookieです。
- セッション管理とは分離している
- Cookieがなくてもサービスは動く場合がある(提携なしのアクセスも許容するなど)
- 特定のAPIでのみ必要なことが多い
この場合は、必要なhandlerで個別に読むか、特定のルートグループにだけmiddlewareを適用するのが自然です。アプリ全体にかけるほどではないことが多いです。
認証用Cookie(セッションIDやトークンなど)
「このリクエストを送ってきたのが誰か」を特定するためのCookieです。
- 認証が通らなければ全て弾く
- ほぼ全ての保護されたルートに必要
- ログインチェックとセットで動く
この場合は、認証middlewareとして共通化するのが向いています。保護対象のルート全体で必要になることが多いためです。
動作確認
レスポンスの考え方も分けておくと分かりやすくなります。
- Cookieがない、形式が壊れている
HTTP入力の問題なので400 Bad Requestを返します。 - 形式は正しいが、業務上は許可されていない
入力としては受理できるが利用は許可しないため、このサンプルでは403 Forbiddenを返します。
# Cookieなし → 400
curl http://localhost:8080/handler/partner-info
# {"error":"partner_code cookieが設定されていません"}
# 形式不正(短すぎる) → 400
curl --cookie "partner_code=ab" http://localhost:8080/handler/partner-info
# {"error":"partner_code cookieの形式が不正です"}
# 形式OK・業務的に不正な値 → 403
curl --cookie "partner_code=unknown-code" http://localhost:8080/handler/partner-info
# {"error":"許可されていない提携コードです: unknown-code"}
# 正常 → 200
curl --cookie "partner_code=partner-a" http://localhost:8080/handler/partner-info
# {"message":"提携先として有効です","partnerCode":"partner-a","status":"active"}
middleware版も同じです。/middleware/partner-info に対して同じcurlを実行すれば確認できます。
どちらを選ぶか
| 状況 | 向くパターン |
|---|---|
| そのAPIだけにCookieが必要 | handler |
| 複数のAPIで同じCookieを使う | middleware(ルートグループに適用) |
| 認証Cookie(ほぼ全ルートに必要) | middleware |
| 識別用Cookie(一部のルートに必要) | handler または限定的なmiddleware |
| Cookieが任意で例外ルートがある | handler |
どちらを選ぶ場合でも、serviceにCookie取得の処理を持ち込まないことは共通です。
serviceはHTTPから独立したビジネスロジックの層です。gin.Context に依存し始めると、テストが書きにくくなり、別の場所で再利用しづらくなります。
まとめ
GinでCookieを扱う場合の責務の分け方を整理すると、こうなります。
- Cookieを読む、形式を確認するのはhandlerまたはmiddlewareの役割です
- その値が業務的に有効かどうかを判定するのはserviceの役割です
- serviceはCookieを知らず、文字列を受け取る形にしておくと責務が崩れません
handler版とmiddleware版を選ぶ判断軸はシンプルです。
- 単発のAPIならhandler
- 複数のAPIで共通ならmiddleware
- 認証用Cookieならmiddleware、識別用Cookieは状況に応じて選ぶ
大事なのは、どこでCookieを読むかより、責務をどこで分けるかです。Cookieはクライアント入力です。業務上の許可判定はserviceで行い、handlerやmiddlewareはHTTP入力の窓口として扱うのが基本です。
Discussion