
CORSとプリフライトリクエストの仕組みを調べたので、備忘録としてまとめます
はじめに
Web開発をしていると、異なるオリジン間でリソースを共有しようとした際に、必ずと言っていいほど「CORSエラー」という壁にぶつかりますよね。API-GWとLambdaでの構築をしていて、このCORSエラーに泣かされてきました。
CORSはブラウザのセキュリティ機能「同一オリジンポリシー」が原因で発生しますが、最近のWebアプリではAPIサーバーとフロントエンドが別ドメインで動くのが当たり前。だから、オリジンを超えたリソース共有は避けて通れません。
この記事では、CORS (Cross-Origin Resource Sharing) と、特に複雑なリクエストで必須となるプリフライトリクエスト (Preflight Request) の仕組みについて、今回改めて調べた内容をまとめてみました。
ほんと、こういう仕組みを理解するのが年齢を重ねていくと困難になってきます。なので、自分向けのアウトプットです。
図はマーメイドで書いてます。エディタだと参照できますが、ブログだと参照できないな・・・マーメイドの何かを入れないと。。。
CORSとは何か?
CORSは「オリジン間リソース共有」と訳されます。これは、ブラウザの同一オリジンポリシーという制約を、安全に緩和するための仕組みなんです。
同一オリジンポリシー
そもそもブラウザは、セキュリティ上の理由から、あるオリジンから読み込まれたドキュメントやスクリプトが、他のオリジンのリソースにアクセスすることを原則として禁止しています。ここでいう「オリジン」とは、プロトコル、ホスト(ドメイン)、ポートの組み合わせを指します。
例えば、https://example.com から https://api.example.com/data にアクセスしようとすると、ホストが異なるため「別オリジン」と判断され、ブラウザによって通信がブロックされます。
graph TD
subgraph "同一オリジン (https://example.com)"
A[<img src="https://example.com/icon.png" />] --> B{<a href="/page">Page</a>};
B --> C[<script src="/script.js"></script>];
end
subgraph "別オリジン (https://api.another.com)"
D[API Server]
end
C -- "XMLHttpRequest" --> D;
style D fill:#f9f,stroke:#333,stroke-width:2px
CORSは、サーバー側が「このオリジンからのアクセスは許可しますよ」という意思表示をHTTPヘッダーを通じて行うことで、この制約を解除する仕組みです。
CORSの仕組み
CORSにおける通信は、大きく「単純リクエスト」と「プリフライトリクエスト」の2種類に分けられます。
1. 単純リクエスト (Simple Request)
以下の条件をすべて満たすリクエストは「単純リクエスト」として扱われ、事前の確認なしに直接リクエストが送信されます。
- メソッドが
GET,HEAD,POSTのいずれか - Content-Typeヘッダーが
application/x-www-form-urlencoded,multipart/form-data,text/plainのいずれか - カスタムヘッダー(
X-My-Headerなど)が含まれない
サーバーはリクエストを受け取ると、レスポンスに Access-Control-Allow-Origin ヘッダーを含めて返します。ブラウザはこのヘッダーを見て、アクセスが許可されているかどうかを判断します。
sequenceDiagram
participant Browser
participant Server
Browser->>Server: GET /resource (Origin: https://client.com)
Note right of Browser: 単純リクエストを送信
Server->>Browser: 200 OK (Access-Control-Allow-Origin: https://client.com)
Note left of Server: 許可するオリジンをヘッダーで通知
Browser->>Browser: レスポンスをJavaScriptに渡す
2. プリフライトリクエスト (Preflight Request)
単純リクエストの条件を満たさない、より複雑なリクエスト(例えば PUT や DELETE メソッド、Content-Type が application/json のリクエストなど)では、実際のリクエストの前にプリフライトリクエストと呼ばれる確認用のリクエストが自動的に送信されます。
これは、ブラウザが「これからこういうメソッドとヘッダーでリクエストを送りたいんだけど、大丈夫?」とサーバーに事前にお伺いを立てるためのものです。
プリフライトリクエストの仕組み
プリフライトリクエストは OPTIONS メソッドで行われます。このリクエストには、実際のリクエストで使用したいメソッドやヘッダーの情報が含まれています。
プリフライトの流れ
ブラウザ → サーバー (OPTIONSリクエスト) ブラウザは、実際のリクエストに関する情報を含む
OPTIONSリクエストを送信します。Access-Control-Request-Method: 実際のリクエストのメソッド (PUTなど)Access-Control-Request-Headers: 実際のリクエストに含まれるカスタムヘッダー (Content-Typeなど)
サーバー → ブラウザ (プリフライトへの応答) サーバーはリクエスト内容を確認し、許可するメソッドやヘッダーをレスポンスヘッダーで返します。
Access-Control-Allow-Origin: アクセスを許可するオリジンAccess-Control-Allow-Methods: 許可するメソッド (GET, POST, PUT, DELETEなど)Access-Control-Allow-Headers: 許可するヘッダー (Content-Typeなど)Access-Control-Max-Age: このプリフライト応答をキャッシュして良い時間(秒)。この期間内はプリフライトリクエストが省略されます。
ブラウザの判断と実際のリクエスト ブラウザはプリフライトの応答を見て、実際のリクエストが許可されていると判断した場合にのみ、本来のリクエスト(この例では
PUT)を送信します。許可されていなければ、その時点で処理は中断され、コンソールにCORSエラーが表示されます。
sequenceDiagram
participant Browser
participant Server
Note over Browser, Server: プリフライト (Preflight)
Browser->>Server: OPTIONS /resource (Origin: https://client.com)<br/>Access-Control-Request-Method: PUT<br/>Access-Control-Request-Headers: Content-Type
Note right of Browser: 「PUTでContent-Typeヘッダー付きのリクエストを送っていい?」
Server->>Browser: 204 No Content<br/>Access-Control-Allow-Origin: https://client.com<br/>Access-Control-Allow-Methods: GET, POST, PUT<br/>Access-Control-Allow-Headers: Content-Type
Note left of Server: 「いいよ。PUTも許可するし、Content-TypeもOK」
Note over Browser, Server: 実際のリクエスト (Actual Request)
Browser->>Server: PUT /resource (Origin: https://client.com)<br/>Content-Type: application/json<br/>{ "data": "hello" }
Note right of Browser: 許可されたので実際のリクエストを送信
Server->>Browser: 200 OK (Access-Control-Allow-Origin: https://client.com)
Note left of Server: リソースを更新し、成功応答を返す
Browser->>Browser: レスポンスをJavaScriptに渡す
AWS API Gateway + LambdaにおけるCORS対応のベストプラクティス
AWS環境、特にAPI GatewayとLambdaプロキシ統合の組み合わせでは、CORS対応の実装方法はいくつかありますが、AWSが推奨するベストプラクティスは、ずばり**「API Gateway側でCORS設定を完結させる」**方法です。
ベストプラクティス: API GatewayでCORSを処理する
プリフライトリクエスト(OPTIONS)と、実際のリクエストへのCORSヘッダー(Access-Control-Allow-Originなど)の付与を、すべてAPI Gatewayの機能で処理する方法です。
なぜこれがベストプラクティスなのか?
Lambdaの責務分離とコードの簡潔化 Lambda関数はビジネスロジックに専念できるようになります。CORSヘッダーを付与する定型的なコードを各関数に書く手間が省け、コードもすっきりします。
コスト効率 API Gatewayは
OPTIONSメソッドに対するリクエストをLambdaに転送せず、自身で応答を返すため、プリフライトリクエストに対するLambdaの呼び出し料金が発生しません。リクエスト数が多いAPIでは、この差は無視できません。設定の一元管理 CORSポリシーを一箇所(API Gateway)で管理できるため、設定の変更や確認が容易になります。複数のLambda関数でCORS設定が分散することを防ぎ、メンテナンス性が向上します。
一次情報源
このアプローチは、AWSの公式ドキュメントやWell-Architected Frameworkの考え方に基づいています。
API Gatewayのドキュメント: API Gateway での CORS の有効化
このドキュメントでは、API GatewayコンソールやOpenAPI定義を用いて、
OPTIONSメソッドのモック統合を設定し、CORSヘッダーを管理する方法が具体的に解説されています。Lambdaを呼び出さずにCORSを処理する方法が公式な手順として示されています。AWS Prescriptive Guidance: Use API Gateway to centralize CORS header management for REST APIs
「規範的ガイダンス」として、このパターンを明確に推奨しています。Lambdaプロキシ統合におけるCORSヘッダー管理を一元化する利点(コスト、メンテナンス性)を強調しています。
次善策: LambdaでCORSを処理する
小規模なアプリケーションや、オリジンを動的に変更する必要があるなど、特別な要件がある場合は、Lambda関数内でCORSヘッダーを返す方法も選択肢となります。しかし、これは一般的にはベストプラクティスとは見なされません。
# LambdaでCORSヘッダーを返す場合の例 (Python)
import json
def lambda_handler(event, context):
# ビジネスロジック
# ...
return {
"statusCode": 200,
"headers": {
"Access-Control-Allow-Origin": "https://your-allowed-origin.com",
"Access-Control-Allow-Methods": "GET, POST, PUT",
"Access-Control-Allow-Headers": "Content-Type, X-Amz-Date, Authorization, X-Api-Key, X-Amz-Security-Token"
},
"body": json.dumps({ "message": "Success!" })
}
この方法は、すべての関数に同様のコードが必要になり、プリフライトリクエストのたびにLambdaが起動するため、コストとメンテナンス性の観点から不利になります。
自分の実装
さて、最近構築したAPI-GWとLambdaの構成では、CORSを両方で処理していました。具体的には、API-GWでプリフライトを、Lambdaで単純リクエストのCORSを処理する形です。
確かに、これだとLambda側でCORSを処理しなければならないので、Lambdaを追加するときにツラみを感じます。また、API-GWのtemplate.yamlにも同様の定義を書かなければならないので、これもツラさを感じます。
まとめ
CORSは、Webのセキュリティを維持しつつ、異なるオリジン間で安全にリソースをやり取りするための重要な仕組みです。特にプリフライトリクエストは、application/json を用いたAPI通信が主流の現代において、避けては通れないものです。
CORSエラーに遭遇した際は、単に Access-Control-Allow-Origin: * を設定して場当たり的に解決するのではなく、なぜエラーが起きているのかを理解し、適切なヘッダーを設定することが重要です。
AWS環境でAPI GatewayとLambdaを利用する場合、API Gatewayの機能を使ってCORSを処理する方法が、コスト、パフォーマンス、メンテナンス性の観点から最も優れたベストプラクティスと言えるでしょう。
と、「まとめ」部分はAIに書いてもらいました。こうやってまとめを書くと、広く伝えている感がある。
