
CORS の仕組みとハマりどころ - プリフライト・credentials・Allow-Origin を理解する
フロントエンドから別ドメインの API を叩いて「No 'Access-Control-Allow-Origin' header is present」に阻まれた経験は、誰しもあると思います。CORS(Cross-Origin Resource Sharing / オリジン間リソース共有)は、仕組みを知らないと延々ハマり、知っていれば数分で解決できる領域です。
この記事では、CORS の仕組みと典型的なハマりどころを、MDN と WHATWG Fetch Standard を一次ソースに整理します。
まず: オリジンと同一オリジンポリシー
オリジンは「スキーム + ホスト + ポート」の3点セットです。3つすべてが一致して初めて「同一オリジン」になります(パスの違いは関係なし)。
基準 http://example.com/dir/a.html との比較 | 判定 |
|---|---|
http://example.com/dir2/b.html | 同一(パスだけ違う) |
https://example.com/a.html | 別オリジン(スキーム違い) |
http://example.com:81/a.html | 別オリジン(ポート違い) |
http://news.example.com/a.html | 別オリジン(ホスト違い) |
同一オリジンポリシーは、あるオリジンのスクリプトが別オリジンのリソースを勝手に読み取れないようにするブラウザの安全機構です。fetch や XMLHttpRequest はこれに従うため、適切な CORS ヘッダがない限りクロスオリジンのレスポンスを JS から読めません。CORS は「サーバが HTTP ヘッダで、どのオリジンからの読み取りを許すかブラウザに伝える」仕組みです。
単純リクエストとプリフライト
CORS には2パターンあります。
単純リクエスト(preflight が飛ばない)
次をすべて満たすと「単純リクエスト」となり、事前確認なしで本リクエストが飛びます。
- メソッドが
GET/HEAD/POSTのいずれか - 手動で付けるヘッダが安全リスト(
Accept/Accept-Language/Content-Language/Content-Type/Range)のみ Content-Typeがapplication/x-www-form-urlencoded/multipart/form-data/text/plainのいずれか
プリフライト(OPTIONS で事前確認)
上記を1つでも外れると、ブラウザは本リクエストの前に OPTIONS メソッドで事前確認(preflight)を送ります。代表的なトリガーは:
PUT/DELETE/PATCHなどのメソッドPOSTでもContent-Type: application/json- 独自ヘッダ(例
X-Custom-Header)を付ける場合
プリフライトでは Access-Control-Request-Method と Access-Control-Request-Headers で「これから何をするか」をサーバに伝えます。
主なヘッダ
サーバが返すレスポンスヘッダ:
| ヘッダ | 役割 |
|---|---|
Access-Control-Allow-Origin | 許可するオリジン(具体的に1つ、または *) |
Access-Control-Allow-Methods | 許可するメソッド(preflight 応答) |
Access-Control-Allow-Headers | 許可するリクエストヘッダ(preflight 応答) |
Access-Control-Allow-Credentials | true で credentials 付きを許可 |
Access-Control-Expose-Headers | JS から読めるようにするレスポンスヘッダ |
Access-Control-Max-Age | preflight 結果をキャッシュする秒数 |
実際の preflight のやりとりはこうなります(MDN の例)。
OPTIONS /doc HTTP/1.1
Origin: https://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type,x-pingotherHTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400Access-Control-Max-Age で preflight をキャッシュすると、毎回 OPTIONS が飛ぶのを抑えられます(上限はブラウザ依存。Chromium は2時間、Firefox は24時間)。
credentials(Cookie・認証情報)付きの注意
デフォルトではクロスオリジンに Cookie 等は送られません。送るには fetch(url, { credentials: 'include' }) を使いますが、サーバ側に追加の条件が要ります。
Access-Control-Allow-Originに*は使えない。具体的なオリジンを返す必要がある-
Access-Control-Allow-Credentials: trueが必須 - credentials 付きでは
Allow-Methods/Allow-Headersの*も「文字どおりのアスタリスク」として扱われるので、明示列挙が必要
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Credentials: true
Vary: OriginNOTE
Authorization ヘッダは特別扱いで、Access-Control-Allow-Headers: *(ワイルドカード)では許可されません。明示的に Authorization と列挙する必要があります(JWT を Bearer トークンで送るときに踏みがち)。
よくあるハマりどころ
- 「No 'Access-Control-Allow-Origin' header」: サーバが CORS ヘッダを返していない。サーバを直せるならヘッダを追加。直せないなら自分で管理するプロキシ経由にする
- 「credentials is not supported if Allow-Origin is '*'」: credentials 付きなのに
*を返している。具体的オリジン +Allow-Credentials: trueに直す Vary: Originの付け忘れ: オリジンごとにAccess-Control-Allow-Originを出し分けるならVary: Origin必須。忘れると、あるオリジン向けの応答がキャッシュされて別オリジンに返り、CORS エラーやキャッシュ汚染が起きる- preflight 未対応: サーバが
OPTIONSに正しく応答しないと本リクエストへ進めない
いちばん大事な勘所: CORS は認可ではない
最後に、最も誤解されやすい点です。CORS はブラウザの仕組みであり、サーバのアクセス制御(認証・認可)ではありません。
- CORS ヘッダは「ブラウザが、クロスオリジンのレスポンスを JS に渡してよいか」を制御するだけ
- サーバ間通信(server-to-server)や
curlは CORS を完全に無視できる - したがって機密データの保護を CORS に頼ってはいけない。保護はサーバ側の認証・認可(と適切な HTTP ステータス)で行う
「CORS を緩めたらデータが漏れる」のではなく、「CORS はそもそもデータ保護の仕組みではない」と理解するのが正解です。
まとめ
- オリジン=スキーム+ホスト+ポート。同一オリジンポリシーが土台で、CORS はサーバが「読み取り許可」をヘッダで伝える仕組み
- 単純リクエスト(GET/HEAD/POST + 安全なヘッダ/Content-Type)以外は preflight(OPTIONS)が飛ぶ
- credentials 付きは
*不可。具体的オリジン +Allow-Credentials: true、Authorizationは明示列挙 - オリジンを出し分けるなら
Vary: Origin。Access-Control-Max-Ageで preflight をキャッシュ - CORS は認可ではない。機密保護はサーバ側の認証・認可で
エラーメッセージの一語一句には意味があります。「どのヘッダが足りないのか」を読めるようになれば、CORS はもう怖くありません。


