この章で行うこと

  • ミドルウェアの実装
    • ロガーミドルウェア
    • 認可制御ミドルウェア
    • CORSミドルウェア(番外編)

ミドルウェアについて

複数のエンドポイントにおいて同じ処理を行いたい時や、アクセスログなど透過的に付け加えたい処理などを実装する際に有用なのが、ミドルウェアパターンです。

ミドルウェアを実装する例として、以下のようなものがあります。

  • 認証・認可(JWTトークンの検証)
  • ロギング(アクセスログの記録)
  • CORS設定(クロスオリジンリクエストの管理)
  • リクエストバリデーション(リクエストデータのチェック)
  • リクエスト・レスポンスのトレース(パフォーマンス測定やデバッグ)

これらのようなミドルウェアは、ハンドラーによる処理の前・後に実行されます。

図からも分かるようにハンドラーと同じく、インターフェースアダプター層におけるプレゼンテーションにミドルウェアの実装を配置することにしました。ハンドラーとミドルウェアは密接に関わるものだからです。

さらに、ミドルウェアがリクエストとレスポンスの流れを制御し、「外側」と「内側」の橋渡しを行うものだとするとハンドラーとそう大きな違いはないと筆者は考えました。

そのため、app/adapter/presentation/middleware/として、ミドルウェアを定義していくことにします。

net/httpにおけるミドルウェア定義

他のwebフレームワークには多様なミドルウェアが用意されていることがありますが、net/httpパッケージには、ミドルウェアが用意されていません。

そのため、欲しいミドルウェアの機能は自身で実装していく必要があります。 golangにおけるミドルウェアパターンは、以下のようなシグネチャを満たすように書いていくのが一般的です。

func(h http.Handler)http.Handler

ミドルウェアを適用させたいハンドラ(h)をラップした新たなハンドラを返すといった構造になっています。

ポイントは、http.Handlerインターフェースを満たすようにミドルウェアを実装していることです。これによって、ミドルウェアを入れ子にすると複数のミドルウェアをハンドラに適用することができます。またhttp.Handlerを返すため、Handleメソッドでルーティングにそのまま登録することができます。この辺についてはまだ分かりにくいと思いますが、実装の中で実感していただければと思います。

https://tutuz-tech.hatenablog.com/entry/2020/03/23/220326

ミドルウェアパターンの実装例

先にあげたfunc(h http.Handler)http.Handlerのシグネチャを満たすためにはどういったふうに書けばいいかというと、以下のようになることが多いです。

func middleware(h http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

        // ミドルウェアで実行したい処理

        h.ServeHTTP(w, r)  // 引数にとったハンドラの処理を実行

        // ミドルウェアで実行したい処理
        
	})
}

http.Handlerを返すために、http.HandlerFuncを返しています。

前章のおさらいとなりますが、http.HandlerFuncは関数型であり、func(w http.ResponseWriter, r *http.Request)といった無名関数(クロージャといいます)をキャストすることでhttp.Handlerインターフェースを満たすようにするのでした。http.HandlerFunc関数型にはServeHTTP()メソッドが定義されているからそれが可能なのでしたね。

ミドルウェアは、引数として受け取ったハンドラーhを内部で実行します(h.ServeHTTP(w, r) )。それ以外の関数内に書いた処理は全てミドルウェアの処理内容となります。そのため、ハンドラー実行の前でも後でも、または両方でも記述することが可能です。

このようなミドルウェアは、以下のようにしてルーティングに登録することができます。

// 何らかのハンドラーにミドルウェアを適用させてルーティングに登録する
mux.Handle("GET /",middleware(handler))

middleware()はhttp.Handlerインターフェースを返すので、Handleメソッドによってルーティングにそのまま登録できるのですね。

このようなミドルウェアパターンは、とてもシンプルで扱いやすいことがわかります。

追加情報を利用したミドルウェアパターンの実装例

少し発展したミドルウェアパターンも今回実装するので、ここで取り上げておきます。

先に示したパターン(func(h http.Handler)http.Handler)だと、「ミドルウェア内で外部の設定情報や依存コンポーネントを使いたい」といった場合に対応しにくいという問題があります。このままだと、http.Handlerを引数にとることになっているからです。

そこで、「返り値にミドルウェアパターンを返す」関数を記述することで、引数に任意の値を取れるようにします。

func middleware(xxx XXX) func(h http.Handler) http.Handler {
	return func(h http.Handler) http.Handler {
		return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {

                    // Xxx型のxxxを利用した何らかの処理
			
                    h.ServeHTTP(rw, r) //ハンドラの処理を実行
		}
            )
	}
}

コードに書いてあるままの説明になってしまいますが、func(h http.Handler) http.Handlerを返すようになっています。

このようなパターンだと、以下のようにルーティングに登録します。

mux.Handle("GET /",middleware(xxx)(handler))

ぱっと見でわかりにくいですが、middleware(xxx)の返り値がfunc(h http.Handler) http.Handlerの関数そのものと考えてみてください。書き方を変えれば、以下のようになります。

m := middleware(xxx)
mux.Handle("GET /",m(handler))

変数mには、middleware(xxx)の返り値であるfunc(h http.Handler) http.Handlerの関数そのものが格納されています。

このパターンは、書籍にて紹介されていました。

https://www.c-r.com/book/detail/1462

ミドルウェアの実装

今回は、ロガーミドルウェアと認可制御ミドルウェアを実装します。上に示したミドルウェアパターンを用いていくので、そこにも注目していただければと思います。

ロガーミドルウェア

エンドポイントを叩いた時に、「リクエスト情報」と「レスポンス情報」を見やすくログに書き込み、デバッグをしやすくすることを目的とします。

POST /usersを叩いた時に、以下のようにログに書き込まれるようにします。

=====================================================================================================
[REQUEST]
Timestamp       : 2025-02-01T12:07:47Z
Method          : POST
Path            : /users
Authorization   :
Body            :
{
  "email": "hoge@hogehogecom",
  "name": "hogehoge",
  "password": "password"
}
-----------------------------------------------------------------------------------------------------
[RESPONSE]
Timestamp       : 2025-02-01T12:07:47Z
Status          : 201
Body            :
{
    "status": 201,
    "data": {
        "id": "01JK0PHW1FYN0YT8A9Z3KWGCMP",
        "email": "hoge@hogehogecom",
        "name": "hogehoge"
    }
}
=====================================================================================================

それぞれ、以下のように項目を設けています。

  • リクエスト情報

    • タイムスタンプ
    • HTTPメソッド
    • リクエストパス
    • Authorizationヘッダ
    • リクエストボディ
  • レスポンス情報

    • タイムスタンプ
    • ステータスコード
    • レスポンスボディ

実装

ロガーミドルウェアのコード
// app/adapter/presentation/middleware/logger.go
package middleware

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"time"
)

// HTTP リクエストとレスポンスをログに記録するミドルウェア
func Logger(h http.Handler) http.Handler {
	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
		// リクエストの処理
		requestLog(r)

		// レスポンス用ラッパーを作成
		buf := &bytes.Buffer{} //レスポンスボディを書き込むためのバッファ
		rww := newResponseWrapper(rw, buf)

		// ハンドラ呼び出し
		// ラッパーをResponseWriterとして渡し、バッファにもレスポンスを書き込むようにする
		h.ServeHTTP(rww, r)

		// レスポンスの処理
		responseLog(rww, buf)
	})
}

// リクエスト情報を取得しログに出力
func requestLog(r *http.Request) {
	reqBody, _ := io.ReadAll(r.Body)
	defer r.Body.Close()

	var formattedReqBody bytes.Buffer
	json.Indent(&formattedReqBody, reqBody, "", "    ")

	// リクエストボディをログに記録
	fmt.Printf(
		"=====================================================================================================\n"+
			"[REQUEST]\n"+
			"Timestamp       : %s\n"+
			"Method          : %s\n"+
			"Path            : %s\n"+
			"Authorization   :%s\n"+
			"Body            :\n%s"+
			"\n-----------------------------------------------------------------------------------------------------\n",
		time.Now().Format(time.RFC3339),
		r.Method,
		r.URL.Path,
		r.Header.Get("Authorization"),
		formattedReqBody.String(),
	)

	// リクエストボディを再代入
	// リクエストボディはストリームなので再代入する必要がある
	r.Body = io.NopCloser(bytes.NewBuffer(reqBody))
}

// レスポンス情報を整形しログに出力
func responseLog(rww *rwWrapper, buf *bytes.Buffer) {
	var formattedRespBody bytes.Buffer
	json.Indent(&formattedRespBody, buf.Bytes(), "", "    ")
	status := rww.statusCode

	fmt.Printf(
		"[RESPONSE]\n"+
			"Timestamp       : %s\n"+
			"Status          : %d\n"+
			"Body            :\n%s"+
			"=====================================================================================================\n",
		time.Now().Format(time.RFC3339),
		status,
		formattedRespBody.String(),
	)
}

// レスポンスのログ記録用にレスポンスをラップ
type rwWrapper struct {
	rw          http.ResponseWriter
	multiWriter io.Writer
	statusCode  int
}

func newResponseWrapper(rw http.ResponseWriter, buf io.Writer) *rwWrapper {
	return &rwWrapper{
		rw:          rw,
		multiWriter: io.MultiWriter(rw, buf),
	}
}

// http.ResponseWriterインターフェースを満たすためのメソッド
func (rww *rwWrapper) Header() http.Header {
	return rww.rw.Header()
}
func (rww *rwWrapper) Write(b []byte) (int, error) {
	return rww.multiWriter.Write(b)
}
func (rww *rwWrapper) WriteHeader(statusCode int) {
	rww.statusCode = statusCode
	rww.rw.WriteHeader(statusCode)
}

少し複雑なので、ポイントごとに記述を追っていきます。

  • ミドルウェアパターンを満たすLogger()関数
  • リクエスト情報をログに書き出すrequestLog()関数
  • レスポンス情報をログに書き出すresponseLog()関数

Logger()関数

ミドルウェアの本体です。ミドルウェアパターンのシグネチャを満たしていることがわかります。

内部的な処理としては、

  • リクエスト情報をロギング→ハンドラーの処理を実行→レスポンス情報をロギング

といった流れになっており、プライベート関数を呼び出しています。

ハンドラー処理を実行した後は、http.ResponseWriter型の変数rwwにレスポンス情報が含まれています。そのrwwを参照することで、レスポンス情報を取得し、ロギングするようにしているのです。前章を振り返りますが、ハンドラー内で呼び出しているプレゼンター関数は、以下のように、http.ResponseWriter型の変数(rw)にヘッダーやステータスコード、JSONを付加していました。

func respondJsonSuccess[T any](rw http.ResponseWriter, statusCode int, respBody T) {
	rw.Header().Set("Content-Type", "application/json;charset=utf-8")
	rw.WriteHeader(statusCode)
	jsonResp := SuccessResponse[T]{
		Status: statusCode,
		Data:   respBody,
	}
	if err := json.NewEncoder(rw).Encode(jsonResp); err != nil {
		RespondInternalServerError(rw, err.Error())
	}
}

図解でも示しておきます。

requestLog()関数

リクエスト情報を含む*http.Request型の変数rを読み取ってfmt.Printf()関数によって標準出力にログを書き込むようにしているといったところが大まかな処理内容です。

しかし、ここで注目すべきはリクエストボディは一度しか読み取れないということです。*http.Request型のリクエストボディはストリームなデータ構造のようです。以下の記事では、そのような構造がコードベースで解説されています。

https://rarejob-tech-dept.hatenablog.com/entry/2021/11/15/100650

上の記事を参考に、リクエストボディが一度しか読み込めない構造を取っているワケは以下のように説明できそうです。

リクエストボディの読み取りは、sawEOFというフラグによって制御されているようです。ボディを最後まで読み取れた時点でフラグがtrueになり、もう一度読み取ろうとしてもsawEOFフラグがtrueの場合はEOFエラーが返るようになっています。

こうしている理由は、リクエストボディを最後まで読み取らなかった場合に、TCPコネクションを再利用しないようにするためです。リクエストボディを中途半端に読み取ったとして、「同じTCP接続を使い回して」後続のリクエストボディを読み取った場合、バッファに前回のリクエストボディが残っているかもしれません。そうなると、正しくリクエストボディを読み取れなくなる可能性が生じます。

その問題を回避するために、

(リクエストボディがCloseされているにも関わらず)sawEOFフラグがfalseの場合、つまりリクエストボディを中途半端に読み取ってしまっている場合には、TCP接続を切断して、次回は新たなTCP接続でやり取りするようにするのです。

リクエストボディを読み取るio.ReadAll(r.Body)が一度しか行えないとなると、後続のハンドラー内の処理でリクエストボディを読み取ることが出来ません。そうなると、ハンドラー内に記述していたJSON→構造体のデコード処理が期待通りに動かなくなってしまいます。

json.NewDecoder(r.Body).Decode(&params)

(dec *json.Decoder) Decodeは内部でリクエストボディを読み取っているからです。

このような問題を解決するため、以下のようにしています。

func requestLog(r *http.Request) {
    // リクエストボディを読み取る
	reqBody, _ := io.ReadAll(r.Body)

    // 省略

	// リクエストボディをログに記録
	fmt.Printf( // 省略)

	// リクエストボディを再代入
	// リクエストボディはストリームなので再代入する必要がある
	r.Body = io.NopCloser(bytes.NewBuffer(reqBody))
}

一度読み取ったリクエストボディを、新たなio.ReadCloser型としてr.Bodyに再設定することで、後続のハンドラーでもリクエストボディを読み取れるようにしています。これを可能にしているのは、以下の部分です。

r.Body = io.NopCloser(bytes.NewBuffer(reqBody))

[]byte型のreqBodyは、はじめに読み取ったリクエストボディです。reqBodyを、bytes.NewBuffer()関数によって*bytes.Buffer型でラップし、io.Readerインターフェースを満たすようにします。[]byte型の変数をio.Readerインターフェースに適用させる際に、この関数はよく使用されます。

func bytes.NewBuffer(buf []byte) *bytes.Buffer
type Buffer struct {
	buf      []byte   // バイトスライスを格納している(ラップしている)
	// 省略
}

そして、r.Bodyはio.ReadCloserインターフェースを満たす必要があります。

type ReadCloser interface {
    Reader
    Closer
}

リクエストボディは、読み取った後にCloseする必要があるからです。

そこで、io.Readerをio.ReadCloserにして返すio.NopCloser()関数を使用します。

func io.NopCloser(r io.Reader) io.ReadCloser

少し複雑ですが、r.Body = io.NopCloser(bytes.NewBuffer(reqBody))によって再設定されたr.Bodyの実態は、[]byte型であるreqBodyをラップした新たなio.ReadCloserです。

そのため、後続のハンドラーにおいて、リクエストボディを読み取る際には、sawEOFフラグによる判定はそもそも存在せず、エラーが起きないのです。

sawEOFフラグなどを使用していたのは、r.BodyにDIされていたio.ReadCloserの具体実装である*http.body型の実装です((b *body) Read()メソッド)。

再代入したのは、*http.body型とは異なる、io.ReadCloserとなっています。厳密に言えば、io.nopCloser型です。r.Body.Read()が呼ばれた時には、io.nopCloserによってラップされている*bytes.Buffer型に定義されたRead()メソッドが呼び出されるということになります。

ここにも、インターフェースという仕組みの柔軟性を感じますね....。

responseLog()関数

関数内を見る前に、Logger()関数内にある次の記述についてまずは解説していきます。

// レスポンス用ラッパーを作成
buf := &bytes.Buffer{} //レスポンスボディを書き込むためのバッファ
rww := newResponseWrapper(rw, buf)

レスポンス情報をロギングするには、レスポンス情報を読み取る必要があります。これは、reqestLog()の場合も同じでした。

しかし、http.ResponseWriterインターフェースには読み取りに関するメソッドが定義されていません。

type ResponseWriter interface {
    Header() Header
    Write([]byte) (int, error)
    WriteHeader(statusCode int)
}

そこで、http.ResponseWriterをラップして、レスポンス情報にアクセスできるようにします。

rww := newResponseWrapper(rw, buf)

newResponseWrapper()関数の内容を見てみます。

// レスポンスのログ記録用にレスポンスをラップ
type rwWrapper struct {
	rw          http.ResponseWriter
	multiWriter io.Writer
	statusCode  int
}

func newResponseWrapper(rw http.ResponseWriter, buf io.Writer) *rwWrapper {
	return &rwWrapper{
		rw:          rw,
		multiWriter: io.MultiWriter(rw, buf),
	}
}

ポイントは、multiWriterフィールドです。

関数内では、multiWriter: io.MultiWriter(rw, buf)としています。

func MultiWriter(writers ...Writer) Writer

このMultiWriter()関数は、書き込み先を複数指定できるようにするものです。以下のようにnewResponseWriter()関数を呼び出している場合、

buf := &bytes.Buffer{}
rww := newResponseWrapper(rw, buf)

*byte.Buffer型のbuf変数にもレスポンスボディが書き込まれるようになるということです。

つまり、http.ResponseWriterと同じくレスポンスボディが書き込まれるbuf変数を読み取ることで、レスポンス情報にアクセスできるようになります。

レスポンス情報の取得についての解決策がわかったところで、responseLog()関数の中身を見てみましょう。

func responseLog(rww *rwWrapper, buf *bytes.Buffer) {
	var formattedRespBody bytes.Buffer
	json.Indent(&formattedRespBody, buf.Bytes(), "", "    ")
	status := rww.statusCode

	fmt.Printf( // 省略 )
}

第二引数に*bytes.Buffer型のbufを受け取っています。これには、レスポンスボディが書き込まれています。以下では、buf.Bytes()によってレスポンスボディ(バイトスライス)を取得し、整形したJSONをformattedRespBodyに格納しています。

var formattedRespBody bytes.Buffer
json.Indent(&formattedRespBody, buf.Bytes(), "", "    ")

また、ステータスコードについては、rwWrapper構造体のstatusCodeフィールドにて管理しています。ステータスコードは、WriteHeader()メソッドにて扱われるため、内部でstatusCodeフィールドに代入することでそれを保持できるようにし、取得できるようにしています。

// http.ResponseWriterインターフェースを満たすためのメソッド
func (rww *rwWrapper) WriteHeader(statusCode int) {
	rww.statusCode = statusCode
	rww.rw.WriteHeader(statusCode)
}
// 省略

単にリクエスト情報・レスポンス情報を取得してロギングするだけと考えると単純そうですが、意外と一筋縄ではいかない部分がありました。

認可制御ミドルウェア

認可制御のビジネスロジック自体は、10章にてユースケースに実装しました。そのため、ユースケース処理を呼び出すように実装を行うという点において、ハンドラーに近い実装となります。

ハンドラーの処理の前に認可処理を行い、不正なリクエストだった場合はミドルウェアの段階でそれを弾きます。

また、ミドルウェアを適用しないハンドラは、無条件にアクセスを通すように出来ます。

認可制御ミドルウェアのコード
// app/adapter/presentation/middleware/authorization.go
package middleware

import (
	"context"
	"errors"
	"net/http"
	"strings"

	"github.com/kakkky/app/adapter/presentation/presenter"
	"github.com/kakkky/app/application/usecase/auth"
)

type userIDKey struct{}

func Authorication(authorizationUsecase *auth.AuthorizationUsecase) func(h http.Handler) http.Handler {
	return func(h http.Handler) http.Handler {
		return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
			jwtToken, err := getJwtToken(r)
			if err != nil {
				presenter.RespondUnAuthorized(rw, err.Error())
				return
			}
			input := auth.AuthorizationInputDTO{JwtToken: jwtToken}
			output, err := authorizationUsecase.Run(r.Context(), input)
			if err != nil {
				presenter.RespondUnAuthorized(rw, err.Error())
				return
			}
			userID := output.UserID
			// コンテキストにuserIDを含める
			ctx := context.WithValue(r.Context(), userIDKey{}, userID)
			// 後続の処理(ハンドラ)を実行する
			h.ServeHTTP(rw, r.WithContext(ctx))
		})
	}
}

func getJwtToken(r *http.Request) (string, error) {
	authorizationHeader := r.Header.Get("Authorization")
	if authorizationHeader == "" {
		return "", errors.New("authorization Header is missing")
	}
	// スペースを区切りとして最大2つに分割
	parts := strings.SplitN(authorizationHeader, " ", 2)
	if len(parts) != 2 || parts[0] != "Bearer" {
		return "", errors.New("invalid Authorization header format")
	}
	return parts[1], nil
}

// コンテキストに設定したユーザーIDを取得する
// ハンドラーで使用する
func GetUserID(context context.Context) string {
	return context.Value(userIDKey{}).(string)
}

認可ユースケース*auth.AuthorizationUsecaseに依存するため、引数に受け取るようにしています。これは、先述したミドルウェアパターンを返り値として返すパターンですね。

処理の流れは以下のようになっています。

  1. リクエストヘッダからJWTトークンを取得
  2. 認可処理(ユースケース)を呼び出す
  3. ユーザーIDをコンテキストに含める
  4. リクエストスコープのコンテキストに、(3)で作成したコンテキストを含めてハンドラーを呼び出す

順を追って見ていきましょう。

getJwtToken()関数

リクエストヘッダー(Authorizationヘッダ)からJWTトークンを抽出する関数です。まず、把握しておくべきは、クライアントからは、JWTトークンを以下のように含めてリクエストが送られてくるということです。

Authorization   : Bearer トークン

このように、Authorizationヘッダに、Bearerスキームを設定します。このBearer認証は、トークンを使用した認証認可に使用されることが想定されているようです。

https://qiita.com/h_tyokinuhata/items/ab8e0337085997be04b1#bearer%E8%AA%8D%E8%A8%BC

関数内部では、このトークンを抽出するためのロジックを書いています。authorizationHeader変数には、Authorizationをキーとして得られた値が入ります。つまり、Bearer トークンの部分が得られます。

authorizationHeader := r.Header.Get("Authorization")
authorizationHeader // "Bearer トークン"

そして、以下でスペースを区切りにして分割したものをparts変数にスライスとして格納します。

parts := strings.SplitN(authorizationHeader, " ", 2)
parts // []string{"Bearer","トークン"}

最後に、pars[1]、つまりトークン部分のみを関数の最終的な返り値としています。

Authorication()関数

ミドルウェアの本体です。

getJwtToken()でトークンを抽出したその後からの処理を見ていきます。

まずは、認可ユースケース処理を呼び出しています。

input := auth.AuthorizationInputDTO{JwtToken: jwtToken}
output, err := authorizationUsecase.Run(r.Context(), input)

認可制御をPASSした場合、ユーザーIDを返す、というのがユースケース処理の内容でした。

後続のハンドラー処理では、そのユーザーIDを元にユーザーを判別したいです。そこで、リクエストスコープのコンテキストにIDを含めることで、後続のハンドラーでそれを参照できるようにしていきます。

context.Context

golangにはcontextパッケージが用されており、context.Context型を通して、「情報を透過的に伝播させる」ことが可能になります。

*http.Requestにも、context.Context型のフィールドが含まれています。

type Request {
  // ctx はクライアントまたはサーバーのコンテキストです。
  // これは、Request 全体を Clone または WithContext を使用してコピーすることでのみ変更されるべきです。
  ctx context.Context
}

これを、本書では「リクエストスコープのコンテキスト」と呼んでいます。「リクエストスコープのコンテキスト」を取得するには、Context()メソッドを使用します。

context.Context型には、キー・バリューで情報を埋め込みます。

// コンテキストにuserIDを含める
ctx := context.WithValue(r.Context(), userIDKey{}, userID)

キーには、userIDKey{}のように空の構造体を設定しています。

type userIDKey struct{}

非公開の独自型userIDKey{}をキーとしてコンテキストに含まれた値を取得するのに、以下のような関数を用意しています。

// コンテキストに設定したユーザーIDを取得する
// ハンドラーで使用する
func GetUserID(context context.Context) string {
	return context.Value(userIDKey{}).(string)
}

コンテキストに与えるキーの衝突を防ぐ

コンテキストは、アプリケーションのロジック内を透過的に扱えるものであると考えれば、異なるパッケージ間でキーが競合し、衝突してしまう可能性があります。

そこで、キーにはプリミティブな値(文字列など)を用いず、独自型で表現することが推奨されています。そうすると、異なるパッケージ間(例えばpackage Aとpackage B)で、同じ型名ctxKey{}を使用したとしても、A.ctxKey{}とB.ctxKey{}といったように異なる型として扱えるようになり、衝突しません。

このように、伝播したい情報(ユーザーID)を含めたコンテキスト(ctx変数)を用意しました。

それを、リクエストスコープのコンテキストに含めることで、後続のハンドラーにてユーザーIDを参照できるようにします。

// 後続の処理(ハンドラ)を実行する
h.ServeHTTP(rw, r.WithContext(ctx))

WithContext()メソッドによって、指定したcontext.Contextを関連付けた新しい*http.Requestを作成しています。

func (r *http.Request) WithContext(ctx context.Context) *http.Request

厳密には、ctxフィールドのみを差し替えた*http.Requestのコピーを返しているようです。

これにより、後続のハンドラーでr.Context()を使って埋め込んだ情報を取得できるようになります。

ハンドラーでは、以下のように記述していましたね(これでエラーが解消されると思います)。

func (pth *PostTaskHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
    // 省略

	// contextからuserIdを取得
	userID := middleware.GetUserID(r.Context())
    
	// 省略
}

複数のミドルウェアを束ねる

さて、ロガー・認可制御の二つのミドルウェアを実装できました。

ミドルウェアは以下のようにしてハンドラーに適用させるのでした。

// 何らかのハンドラーにミドルウェアを適用させてルーティングに登録する
mux.Handle("GET /",middleware(handler))

一つのミドルウェアを適用させるだけならこれでも問題なさそうですが、ミドルウェアが複数に増えた時、以下のように書かなければならず、可読性が下がります。

mux.Handle("/", middlewareA(middlewareB(middlewareC(handler))))

これを解決するために、複数のミドルウェアを扱いやすくするためのロジックを用意しましょう。これは、ルーティングの際に使用するため、実装するディレクトリはpresentation/middlewareではなく、app/infrastructure/routerとします。

composeMiddleware()関数としてロジックを実装していきます。

composeMiddleware() のコード
// app/infrastructure/router/middlewares.go
package router

import "net/http"


// 適用させたい順で、ミドルウェアを引数に入れる
// composeMiddlewares(M1,M2,M3)とした場合、M1(M2(M3()))といったようにラップされたミドルウェアを返す
func composeMiddlewares(middlewares ...func(http.Handler) http.Handler) func(http.Handler) http.Handler {
	return func(h http.Handler) http.Handler {
		for i := range middlewares {
			h = middlewares[len(middlewares)-(i+1)](h)
		}
		return h
	}
}

テストコード

// app/infrastructure/router/middlewares_test.go
package router

import (
	"net/http"
	"net/http/httptest"
	"testing"
)

func TestComposeMiddlewares(t *testing.T) {
	middleware1 := func(h http.Handler) http.Handler {
		return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
			rw.Header().Set("Middleware", "1")
			h.ServeHTTP(rw, r)
		})
	}
	middleware2 := func(h http.Handler) http.Handler {
		return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
			rw.Header().Set("Middleware", "2")
			h.ServeHTTP(rw, r)
		})
	}
	handler := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {})

	sut := composeMiddlewares(middleware1, middleware2)(handler)
	r := httptest.NewRequest(http.MethodGet, "/", nil)
	rw := httptest.NewRecorder()
	sut.ServeHTTP(rw, r)
	// 最終的なMiddrewareヘッダーは2である。
	// リクエスト処理は、Middleware1→Middleware2と行われる
	if got := rw.Header().Get("Middleware"); got != "2" {
		t.Errorf("expected header Middleware to be '2', got '%s'", got)
	}
}

可変長にミドルウェアを受け取れるようにしており、引数にとった最後のミドルウェアから順にハンドラーhに適用させていきます。

ただ、ミドルウェアの実行順序には注意が必要です。実装は次章になりますが、認可制御ミドルウェアとロガーミドルウェアをハンドラーに適用した際の挙動を見ていきましょう。

ルーティングでは、以下のようなコードになります。適用されているミドルウェアがとても見やすいですね。

mux.Handle("GET /users", composeMiddlewares(logger, authorization)(getUsersHandler))

この場合、ミドルウェアは以下のように適用されています。

logger(authorization(getUsersHandler))

実行順序としては、

  1. ロガーミドルウェアがリクエストログを書き込む(requestLog())
  2. 認可制御ミドルウェアが処理を実行
  3. ハンドラーの処理を実行(getUsersHandlerのServeHTTP())
  4. ロガーミドルウェアがレスポンスログを書き込む(responseLog())

といったふうになります。図解で表すと以下のようになります。

ロガーミドルウェアは、ハンドラの処理(ServeHTTP())の前後に処理を挟んでいました。そのため、最初にrequestLog()が実行されます。

その次には、ServeHTTP()が実行されるわけですが、logger()の引数にはauthorization(getUsersHandler)をとっています。ということは、ServeHTTP()によって、次は認可制御ミドルウェアの処理に移るということになるわけです。

認可制御ミドルウェアでは、ハンドラーの処理(ServeHTTP())の前にロジックを記述していましたね。そのため、認可制御のロジックの後で、ハンドラーの処理に移るのです。

ハンドラーの処理が完了すれば、処理はロガーミドルウェアに戻ってきます。ServeHTTP()の後に記述しているロジック(responseLog())が実行され、終了します。

番外編: CORSミドルウェアを実装する

フロントエンドからこのバックエンドAPIを叩く場合、CORS(Cross-Origin Resource Sharing) の設定を行う必要があります。

今回は、フロントエンドからAPIを叩かないため、CORSミドルウェアの実装は番外編としました。ですので、実装する必要はありませんし、このハンズオンでは以降登場しません。

GitHubのリポジトリにおいているソースコードではミドルウェアを登録している記述があります。

// app/adapter/presentation/middleware/cors.go
package middleware

import (
	"net/http"

	"github.com/kakkky/app/adapter/presentation/presenter"
)

func Cors(h 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-Headers", "Content-Type, X-Requested-With, Origin, X-Csrftoken, Accept, Authorization")
		w.Header().Set("Access-Control-Allow-Methods", "POST, GET, PATCH, DELETE, OPTIONS")
		h.ServeHTTP(w, r)
	})
}
https://developer.mozilla.org/ja/docs/Web/HTTP/CORS

現段階のディレクトリ

.
├── .github
│   └── workflows
│       └── test.yml
├── .gitignore
├── Dockerfile.app
├── Makefile
├── app
│   ├── .air.toml
│   ├── adapter
│   │   ├── presentation
│   │   │   ├── handler
│   │   │   │   ├── auth
│   │   │   │   │   ├── login_handler.go
│   │   │   │   │   ├── login_models.go
│   │   │   │   │   └── logout_handler.go
│   │   │   │   ├── health
│   │   │   │   │   ├── handler.go
│   │   │   │   │   └── health_models.go
│   │   │   │   ├── task
│   │   │   │   │   ├── delete_task_handler.go
│   │   │   │   │   ├── get_task_handler.go
│   │   │   │   │   ├── get_task_models.go
│   │   │   │   │   ├── get_tasks_handler.go
│   │   │   │   │   ├── get_user_tasks_handler.go
│   │   │   │   │   ├── get_user_tasks_models.go
│   │   │   │   │   ├── post_task_handler.go
│   │   │   │   │   ├── post_task_models.go
│   │   │   │   │   ├── update_task_state_handler.go
│   │   │   │   │   └── update_task_state_models.go
│   │   │   │   └── user
│   │   │   │       ├── delete_user_handler.go
│   │   │   │       ├── get_users_handler.go
│   │   │   │       ├── get_users_models.go
│   │   │   │       ├── post_user_handler.go
│   │   │   │       ├── post_user_models.go
│   │   │   │       ├── update_user_handler.go
│   │   │   │       └── update_user_models.go
│   │   │   ├── middleware
│   │   │   │   ├── authorization.go
│   │   │   │   └── logger.go
│   │   │   └── presenter
│   │   │       ├── failure.go
│   │   │       └── success.go
│   │   ├── queryservice
│   │   │   ├── interface_querier.go
│   │   │   └── task_queryservice.go
│   │   └── repository
│   │       ├── interface_kvs_commander.go
│   │       ├── interface_querier.go
│   │       ├── task_repository.go
│   │       ├── token_authenticator_repository.go
│   │       └── user_repository.go
│   ├── application
│   │   └── usecase
│   │       ├── auth
│   │       │   ├── authorization_usecase.go
│   │       │   ├── authorization_usecase_dto.go
│   │       │   ├── authorization_usecase_test.go
│   │       │   ├── interface_jwt_authenticator.go
│   │       │   ├── interface_jwt_authenticator_repository.go
│   │       │   ├── login_usecase.go
│   │       │   ├── login_usecase_dto.go
│   │       │   ├── login_usecase_test.go
│   │       │   ├── logout_usecase.go
│   │       │   ├── logout_usecase_dto.go
│   │       │   ├── logout_usecase_test.go
│   │       │   ├── mock_jwt_authenticator.go
│   │       │   └── mock_jwt_authenticator_repository.go
│   │       ├── task
│   │       │   ├── create_task_usecase.go
│   │       │   ├── create_task_usecase_dto.go
│   │       │   ├── create_task_usecase_test.go
│   │       │   ├── delete_task_usecase.go
│   │       │   ├── delete_task_usecase_dto.go
│   │       │   ├── delete_task_usecase_test.go
│   │       │   ├── fetch_task_usecase.go
│   │       │   ├── fetch_task_usecase_dto.go
│   │       │   ├── fetch_task_usecase_test.go
│   │       │   ├── fetch_tasks_usecase.go
│   │       │   ├── fetch_tasks_usecase_test.go
│   │       │   ├── fetch_user_tasks_usecase.go
│   │       │   ├── fetch_user_tasks_usecase_dto.go
│   │       │   ├── fetch_user_tasks_usecase_test.go
│   │       │   ├── interface_task_query_service.go
│   │       │   ├── mock_task_query_service.go
│   │       │   ├── update_task_state_usecase.go
│   │       │   ├── update_task_state_usecase_dto.go
│   │       │   └── update_task_state_usecase_test.go
│   │       └── user
│   │           ├── fetch_users_usecase.go
│   │           ├── fetch_users_usecase_dto.go
│   │           ├── fetch_users_usecase_test.go
│   │           ├── register_usecase.go
│   │           ├── register_usecase_dto.go
│   │           ├── register_usecase_test.go
│   │           ├── unregister_usecase.go
│   │           ├── unregister_usecase_dto.go
│   │           ├── unregister_usecase_test.go
│   │           ├── update_profile_usecase.go
│   │           ├── update_profile_usecase_dto.go
│   │           └── update_profile_usecase_test.go
│   ├── config
│   │   ├── config.go
│   │   └── config_test.go
│   ├── docs
│   │   ├── docs.go
│   │   ├── swagger.json
│   │   └── swagger.yaml
│   ├── domain
│   │   ├── errors
│   │   │   ├── errors.go
│   │   │   └── errors_test.go
│   │   ├── task
│   │   │   ├── interface_task_repository.go
│   │   │   ├── mock_task_repository.go
│   │   │   ├── task.go
│   │   │   ├── task_content.go
│   │   │   ├── task_state.go
│   │   │   └── task_test.go
│   │   └── user
│   │       ├── interface_user_domain_service.go
│   │       ├── interface_user_repository.go
│   │       ├── mock_user_domain_service.go
│   │       ├── mock_user_repository.go
│   │       ├── user.go
│   │       ├── user_domain_service.go
│   │       ├── user_domain_service_test.go
│   │       ├── user_email.go
│   │       ├── user_hashed_password.go
│   │       ├── user_test.go
│   │       └── users.go
│   ├── go.mod
│   ├── go.sum
│   ├── infrastructure
│   │   ├── auth
│   │   │   ├── certificate
│   │   │   │   ├── private.pem
│   │   │   │   └── public.pem
│   │   │   ├── jwt_authenticator.go
│   │   │   ├── jwt_authenticator_test.go
│   │   │   └── key_parse.go
│   │   ├── db
│   │   │   ├── connect.go
│   │   │   ├── container
│   │   │   │   ├── db.go
│   │   │   │   └── dockertest.go
│   │   │   ├── migrations
│   │   │   │   ├── 000001_create_users_table.down.sql
│   │   │   │   ├── 000001_create_users_table.up.sql
│   │   │   │   ├── 000002_create_tasks_table.down.sql
│   │   │   │   └── 000002_create_tasks_table.up.sql
│   │   │   ├── mysql
│   │   │   │   └── conf.d
│   │   │   │       └── my.conf
│   │   │   ├── sqlc
│   │   │   │   ├── db.go
│   │   │   │   ├── models.go
│   │   │   │   ├── queries
│   │   │   │   │   ├── tasks.sql
│   │   │   │   │   └── users.sql
│   │   │   │   ├── sqlc.yml
│   │   │   │   ├── sqlc_querier.go
│   │   │   │   ├── tasks.sql.go
│   │   │   │   └── users.sql.go
│   │   │   └── testhelper
│   │   │       └── fixture.go
│   │   ├── kvs
│   │   │   ├── redis_client.go
│   │   │   ├── redis_commander.go
│   │   │   └── redis_test_client.go
│   │   ├── queryservice_test
│   │   │   ├── queryservice_test.go
│   │   │   ├── task_queryservice_test.go
│   │   │   └── testdata
│   │   │       └── fixtures
│   │   │           ├── tasks.yml
│   │   │           └── users.yml
│   │   ├── repository_test
│   │   │   ├── jwt_authenticator_repository_test.go
│   │   │   ├── repository_test.go
│   │   │   ├── task_repository_test.go
│   │   │   ├── testdata
│   │   │   │   └── fixtures
│   │   │   │       ├── tasks.yml
│   │   │   │       └── users.yml
│   │   │   └── user_repository_test.go
│   │   └── router
│   │       ├── middlewares.go
│   │       └── middlewares_test.go
│   ├── main.go
│   └── tmp
│       ├── build-errors.log
│       └── main
├── compose.yml
├── go.work
├── go.work.sum
└── pkg
    ├── go.mod
    ├── go.sum
    ├── hash
    │   ├── hash.go
    │   └── hash_test.go
    ├── ulid
    │   ├── ulid.go
    │   └── ulid_test.go
    └── validation
        └── validator.go