🦍

gorilla/muxを使って特定のURLパスにだけミドルウェアを機能させる方法

2022/05/07に公開

Go言語のHTTPサーバーのルーティングライブラリgorilla/muxを使って特定のURLパスにミドルウェアを効かせる方法

困ったこと

	http.Handle("/", router)

	// http://www.gorillatoolkit.org/pkg/mux
	router.HandleFunc("/anime/v1/master/cours", coursHandler).Methods("GET")

	router.HandleFunc("/anime/v1/master/{year_num:[0-9]{4}}", yearTitleHandler).Methods("GET")

	router.HandleFunc("/anime/v1/master/{year_num:[0-9]{4}}/{cours:[1-4]}", animeAPIReadHandler).Methods("GET")

	// 管理者だけが実行できるようにしたい、キャッシュ全クリア 環境変数 認証キーあり
	router.HandleFunc("/clear", cacheClear).Methods("POST")
	// 管理者だけが実行できるようにしたい、キャッシュ全再取得 環境変数 認証キーあり
	router.HandleFunc("/refresh", cacheRefresh).Methods("POST")

	// middlewareAdminAuthAPIでAPIキーで管理者認証する
	router.Use(middlewareAdminAuthAPI)

5つのルーティングのうち/clearと/refreshだけは管理者だけが実行できるようにしたいのでmiddlewareAdminAuthAPIというミドルウェアを作りルーティングに追加する。
ただこれだと上の3つの通常のルーティングも巻き込まれてしまう。
gorillaのgithubのTOPページではミドルウェアでログを出力するサンプル(goのhttpサーバーミドルウェアのスタンダードな例)しかなく、パスグループごとにミドルウェアを効かせる方法が書いていなかった。

middlewareAdminAuthAPIの中身はこんな感じ

func middlewareAdminAuthAPI(next http.Handler) http.Handler {
	const APIKEY_HEADER_NAME = "X-API-KEY"
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		rApiKey := r.Header.Get(APIKEY_HEADER_NAME)

		# apikeyはenvやconfで設定
		if apikey != "" && rApiKey == apikey {
			next.ServeHTTP(w, r)
		} else {
			//nolint:errcheck
			w.Write([]byte("authentication error\n"))
		}
	})
}

解決方法 Subrouterを使う

	http.Handle("/", router)

	// http://www.gorillatoolkit.org/pkg/mux
	router.HandleFunc("/anime/v1/master/cours", coursHandler).Methods("GET")

	router.HandleFunc("/anime/v1/master/{year_num:[0-9]{4}}", yearTitleHandler).Methods("GET")

	router.HandleFunc("/anime/v1/master/{year_num:[0-9]{4}}/{cours:[1-4]}", animeAPIReadHandler).Methods("GET")

	// https://github.com/gorilla/mux/issues/445
	admin := router.PathPrefix("/anime/v1/master/cache").Subrouter()

	// キャッシュ全クリア 環境変数 認証キーあり
	admin.HandleFunc("/clear", cacheClear).Methods("POST")
	// キャッシュ全再取得 環境変数 認証キーあり
	admin.HandleFunc("/refresh", cacheRefresh).Methods("POST")

	admin.Use(middlewareAdminAuthAPI)

Subrouterを使うと新たにmux.Routerを親ルータの設定を元にコピーして生成してくれる。
そのサブルーターにのみミドルウェアを設定することで実現できる。

Subrouterの実装

func (r *Route) Subrouter() *Router {
	// initialize a subrouter with a copy of the parent route's configuration
	router := &Router{routeConf: copyRouteConf(r.routeConf), namedRoutes: r.namedRoutes}
	r.addMatcher(router)
	return router
}

一応まったく新しいルーターを生成しても実現できたがサブルーターを追加するほうがいいようだ。

結果的に管理者だけがキャッシュクリアできるAPIパスの実装が実現できた

curl -XPOST --header 'X-API-KEY:abcde' http://localhost:8080/anime/v1/master/cache/cler

[OK] Clear Cache

パス側の実装、ミドルウェア側で認証をするのでパス側の関数では認証コードは必要なくなる

// APメモリキャッシュやmemcached、redisなどのキャッシュを管理者のみがクリアする処理
func cacheClear(w http.ResponseWriter, r *http.Request) {
	cacheBases = make(map[int][]byte)
	cacheBasesWithOgp = make(map[int][]byte)
	w.Write([]byte("[OK] Clear Cache\n"))
}

Discussion