Go WebサーバーにおけるカスタムCORSミドルウェアの構築
Daniel Hayes
Full-Stack Engineer · Leapcell

はじめに
現代のWebでは、Webアプリケーションが異なるドメインでホストされているリソースとやり取りすることはますます一般的になっています。app.example.comから提供されるフロントエンドアプリケーションがapi.example.comのバックエンドサービスにAPIコールを行ったり、サードパーティサービスと統合したりすることを考えてみてください。この一般的なシナリオは、Webブラウザによって強制される基本的なセキュリティメカニズムである同一オリジンポリシー(Same-Origin Policy)にすぐに直面します。このポリシーはセキュリティにとって不可欠ですが、正当なオリジン間やり取りを妨げ、「CORSによってブロックされた」というフラストレーションのたまるエラーにつながる可能性があります。これを克服するために、オリジン間リソース共有(CORS)メカニズムが導入され、サーバーがオリジン間リクエストの許可を明示的に付与できるようになりました。多くのGo Webフレームワークは組み込みのCORSソリューションを提供していますが、CORSミドルウェアを理解して手動で実装することは、その仕組みに対する貴重な洞察を提供し、優れた柔軟性をもたらし、アプリケーションのニーズに正確に合わせてセキュリティポリシーを調整するために不可欠です。この記事では、Go WebサーバーでCORSミドルウェアを手動で実装および設定するプロセスを案内します。
カスタムCORSミドルウェアの理解と実装
コードに飛び込む前に、議論にとって不可欠なCORSに関連するいくつかのコアコンセプトを定義しましょう。
コア用語
- 同一オリジンポリシー(SOP - Same-Origin Policy): 1つのオリジンからロードされたドキュメントまたはスクリプトが、別のオリジンのリソースとやり取りする方法を制限する基本的なセキュリティコンセプト。オリジンは、URLのプロトコル、ホスト、ポートによって定義されます。
- オリジン間リクエスト(Cross-Origin Request): 要求元のドキュメントのオリジンとは異なるオリジンを持つリソースへの要求。
- CORS(Cross-Origin Resource Sharing): WebブラウザとサーバーがWebページがオリジン間リクエストを行えるかどうかを判断するために使用するHTTPヘッダーのセット。
- プリフライトリクエスト(Preflight Request): 特定の「複雑な」リクエスト(例:
GET、HEAD、単純なコンテンツタイプを持つPOST以外のHTTPメソッド、またはカスタムヘッダーを備えたリクエスト)の場合、ブラウザは実際の要求の前にサーバーに自動的な「プリフライト」OPTIONSリクエストを送信します。このプリフライトは、後続の実際の要求に対してサーバーが許可するCORSポリシーを確認します。 - CORSヘッダー:
Access-Control-Allow-Origin:リソースへのアクセスが許可されるオリジンを示します。特定のオリジンまたは任意のオリジンに対して*を指定できます。Access-Control-Allow-Methods:オリジン間リクエストで許可されるHTTPメソッド(例:GET、POST、PUT、DELETE)を指定します。Access-Control-Allow-Headers:実際の要求で使用できるHTTPヘッダーのリスト。Access-Control-Allow-Credentials:オリジン間要求で資格情報(Cookie、HTTP認証)を送信すべきかどうかを示します。サポートされている場合はtrueに設定する必要があります。Access-Control-Expose-Headers:サーバーがフロントエンドJavaScriptコードに公開することを許可するヘッダーをホワイトリストに登録できます。Access-Control-Max-Age:プリフライトリクエストの結果をキャッシュできる期間を示します。
CORSミドルウェアの原則
CORSミドルウェアは概念的には、受信HTTPリクエストとアプリケーションのハンドラーの間に配置されます。その主な責務は、リクエスト、特にOriginヘッダーを検査し、定義済みのルールに基づいて、適切なCORSヘッダーをレスポンスに追加することです。また、OPTIONSプリフライトリクエストを明示的に処理する必要もあります。
カスタムCORSミドルウェアの実装
柔軟なCORSミドルウェアをGoで構築しましょう。許可されるオリジン、メソッド、ヘッダーを管理するための設定構造体(struct)を定義します。
package main import ( "log" "net/http" "strings" "time" ) // CORSConfig はCORSミドルウェアの設定を格納します。 type CORSConfig struct { AllowedOrigins []string AllowedMethods []string AllowedHeaders []string ExposedHeaders []string AllowCredentials bool MaxAge time.Duration // Access-Control-Max-Age ヘッダーの期間 } // NewCORSConfig はデフォルトのCORS設定を作成します。 func NewCORSConfig() *CORSConfig { return &CORSConfig{ AllowedOrigins: []string{"*"}, // デフォルトですべてのオリジンを許可(本番では注意) AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, AllowedHeaders: []string{"Accept", "Content-Type", "Content-Length", "Accept-Encoding", "X-CSRF-Token", "Authorization"}, ExposedHeaders: []string{} , AllowCredentials: false, MaxAge: 10 * time.Minute, } } // CORSMiddleware は、CORS機能を提供するために別の http.Handler をラップする http.Handler です。 func CORSMiddleware(config *CORSConfig, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origin := r.Header.Get("Origin") if origin == "" { // CORSリクエストではありません、スルーします next.ServeHTTP(w, r) return } // オリジンが許可されているか確認します isOriginAllowed := false if len(config.AllowedOrigins) == 1 && config.AllowedOrigins[0] == "*" { isOriginAllowed = true // すべてのオリジンが許可されています } else { for _, allowed := range config.AllowedOrigins { if allowed == origin { isOriginAllowed = true break } } } if !isOriginAllowed { // オリジンが許可されていない場合、CORSヘッダーを追加せず、リクエストを拒否します log.Printf("CORS: Origin '%s' not allowed.", origin) w.WriteHeader(http.StatusForbidden) return } // Access-Control-Allow-Origin ヘッダーを追加します if len(config.AllowedOrigins) == 1 && config.AllowedOrigins[0] == "*" { w.Header().Set("Access-Control-Allow-Origin", "*") } else { w.Header().Set("Access-Control-Allow-Origin", origin) } // 資格情報が許可されている場合、ヘッダーを設定します if config.AllowCredentials { w.Header().Set("Access-Control-Allow-Credentials", "true") } // プリフライト OPTIONS リクエストを処理します if r.Method == http.MethodOptions { // Access-Control-Allow-Methods ヘッダーを追加します w.Header().Set("Access-Control-Allow-Methods", strings.Join(config.AllowedMethods, ", ")) // 要求されたヘッダーに基づいて Access-Control-Allow-Headers ヘッダーを追加します requestedHeaders := r.Header.Get("Access-Control-Request-Headers") if requestedHeaders != "" { allowedRequestHeaders := make([]string, 0) for _, reqHeader := range strings.Split(requestedHeaders, ",") { reqHeader = strings.TrimSpace(reqHeader) for _, allowedConfigHeader := range config.AllowedHeaders { if strings.EqualFold(reqHeader, allowedConfigHeader) { allowedRequestHeaders = append(allowedRequestHeaders, reqHeader) break } } } if len(allowedRequestHeaders) > 0 { w.Header().Set("Access-Control-Allow-Headers", strings.Join(allowedRequestHeaders, ", ")) } else { // 要求されたヘッダーが明示的に許可されていない場合、ヘッダーなしで応答します(ブラウザが拒否します) log.Printf("CORS: No requested headers allowed for origin '%s'. Requested: %s", origin, requestedHeaders) w.WriteHeader(http.StatusForbidden) return } } else { // 特定のヘッダーが要求されていない場合、設定済みの許可ヘッダーを送信するだけです w.Header().Set("Access-Control-Allow-Headers", strings.Join(config.AllowedHeaders, ", ")) } // Access-Control-Max-Age を設定します if config.MaxAge > 0 { w.Header().Set("Access-Control-Max-Age", string(config.MaxAge/time.Second)) } // プリフライトに 204 No Content で応答します w.WriteHeader(http.StatusNoContent) return } // 実際の要求に対して、公開ヘッダーが設定されていれば設定します if len(config.ExposedHeaders) > 0 { w.Header().Set("Access-Control-Expose-Headers", strings.Join(config.ExposedHeaders, ", ")) } // 次のハンドラーにリクエストを渡します next.ServeHTTP(w, r) }) }
CORSミドルウェアロジックの説明:
- リクエストオリジンチェック: まず
Originヘッダーをチェックします。存在しない場合、オリジン間リクエストではないため、CORSヘッダーなしで次のハンドラーへの処理を続行します。 - 許可オリジンの検証:
Originヘッダーをconfig.AllowedOriginsと比較します。*が指定されている場合、すべてのオリジンが許可されます。それ以外の場合、特定のオリジンが許可リストにあるかどうかを確認します。許可されていない場合、リクエストはhttp.StatusForbiddenで拒否されます。 Access-Control-Allow-Origin: このヘッダーは、設定に応じて、要求された特定の許可されているOriginまたは*で設定されます。Access-Control-Allow-Credentials:config.AllowCredentialsがtrueの場合、このヘッダーが追加されます。注意:Access-Control-Allow-Origin: *はAccess-Control-Allow-Credentials: trueと一緒に使用することはできません。現在のAccess-Control-Allow-Originロジックでは、資格情報が許可されている場合に特定のoriginを設定することでこれを処理します。- プリフライト
OPTIONSリクエスト: リクエストメソッドがOPTIONSの場合:Access-Control-Allow-Methodsをconfig.AllowedMethodsに基づいて設定します。- クライアントから
Access-Control-Request-Headersを読み取ります。次に、要求された各ヘッダーがconfig.AllowedHeadersにあるかどうかを確認し、許可されている要求されたヘッダーのみを含むAccess-Control-Allow-Headersレスポンスヘッダーを構築します。カスタムヘッダーが要求されたが許可されていない場合、プリフライトは失敗します。 Access-Control-Max-Ageはconfig.MaxAgeに基づいて設定されます。- ミドルウェアは
http.StatusNoContent(204)で応答し、プリフライトリクエストにはボディが期待されないため、リクエストを終了します。
- 実際の要求: その他のHTTPメソッド(実際の要求)の場合、
Access-Control-Expose-Headersが設定されていれば設定され、クライアントサイドJavaScriptは特定のレスポンスヘッダーにアクセスできるようになります。次に、リクエストは実際のビジネスロジックのためにnextハンドラーに渡されます。
Goサーバーへのミドルウェアの統合
実際のAPIハンドラーでこのCORSMiddlewareを使用する方法の例を次に示します。
package main import ( "encoding/json" "fmt" "log" "net/http" "time" ) // MyHandler はデモンストレーション用のシンプルなハンドラーです。 func MyHandler(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"message": "Hello from Go API!"}) return } if r.Method == http.MethodPost { var data map[string]interface{} if err := json.NewDecoder(r.Body).Decode(&data); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } log.Printf("Received POST data: %+v", data) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "received", "data": fmt.Sprintf("%+v}, data)"}) return } http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } func main() { // CORS の設定 corsConfig := NewCORSConfig() corsConfig.AllowedOrigins = []string{ "http://localhost:3000", // 例: フロントエンドのオリジン "http://127.0.0.1:3000", } corsConfig.AllowedMethods = []string{"GET", "POST", "PUT", "DELETE"} corsConfig.AllowedHeaders = []string{"Content-Type", "Authorization"} // カスタム Authorization ヘッダーを許可 corsConfig.AllowCredentials = true // Cookie/認証ヘッダーを許可 corsConfig.MaxAge = 1 * time.Hour // プリフライトを1時間キャッシュ // 新しい ServeMux を作成します mux := http.NewServeMux() // CORS ミドルウェアをハンドラーに適用します // 順序が重要です: CORS ミドルウェアは実際のハンドラーをラップする必要があります。 corsHandler := CORSMiddleware(corsConfig, http.HandlerFunc(MyHandler)) mux.Handle("/api/data", corsHandler) // デモンストレーションのために CORS ミドルウェアなしの別のハンドラー mux.HandleFunc("/public", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "This is a public endpoint, no CORS applied here.") }) log.Println("Server starting on port 8080...") if err := http.ListenAndServe(":8080", mux); err != nil { log.Fatalf("Server failed: %v", err) } }
main関数で:
CORSConfigを初期化し、カスタマイズします。本番環境では、AllowedOriginsは*ではなく、フロントエンドドメインに固有であるべきです。フロントエンドがオリジン間リクエストでCookieまたはHTTP認証ヘッダーを送信する必要がある場合は、AllowCredentialsをtrueに設定する必要があります。mux(ルーター)を作成します。- カスタム設定を使用して
MyHandlerをCORSMiddlewareでラップします。 /api/dataへのリクエストを処理するためにcorsHandlerを登録します。これは、/api/dataへのすべてリクエストが最初にCORSロジックを通過することを意味します。
アプリケーションシナリオ
- シングルページアプリケーション(SPA): フロントエンド(例:React、Angular、Vue)が1つのドメインで実行され、別のバックエンドAPIからデータを取得する一般的なシナリオ。
- マイクロサービス: サービスが、より大きな分散システム内で異なるドメインまたはポート間で通信する場合。
- サードパーティAPI統合: フロントエンドが異なるドメインのAPIを呼び出す場合、そのAPIのサーバーはCORSを実装する必要があります。逆に、APIがサードパーティによって消費される場合、これが必要になります。
- 開発環境: 多くの場合、開発サーバーは開発バックエンドとは異なるポートで実行され、CORSが必要になります。
結論
Go WebサーバーでCORSミドルウェアを手動で実装および設定することにより、Webセキュリティと機能の重要な側面であるオリジン間リソース共有をきめ細かく制御できます。プリフライトリクエストや特定のHTTPヘッダーなどのCORSのコアコンセプトを理解し、ミドルウェアを慎重に作成することで、開発者はアプリケーションが異なるオリジン間で安全かつ効果的に通信できるようにすることができます。このアプローチは柔軟性を提供し、理解を深め、アプリケーション固有の要件に合わせて調整された複雑なオリジン間課題に対処できるようにし、堅牢で安全なWebインタラクションを保証します。

