この記事で伝えること
フロントエンド開発をしていると、ブラウザのコンソールに次のようなエラーが出て困った経験がある人は多いと思います。
Access to fetch at 'https://api.example.com/data' from origin 'https://app.example.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
このエラーの正体である CORS(Cross-Origin Resource Sharing) について、
- なぜこの制限が存在するのか
- 「オリジン」とは何を指すのか
- ブラウザとサーバーの間で実際に何が起きているのか
- プリフライトリクエストとは何か
を、仕組みのレベルから丁寧に解説します。読み終わった頃には、CORSエラーを見ても「何が起きているか」を具体的に説明できるようになっているはずです。
そもそも「オリジン」とは何か
CORSを理解する第一歩は「オリジン(origin)」の定義を正確に知ることです。オリジンは次の3つの組み合わせで決まります。
-
スキーム(
http/https) -
ホスト(
example.comなど) -
ポート(
:443など、省略時はデフォルトポート)
この3つが1つでも違えば「別オリジン」とみなされます。
| URL A | URL B | 同一オリジン? |
|---|---|---|
https://example.com/a |
https://example.com/b |
○(パスは関係ない) |
https://example.com |
http://example.com |
×(スキームが違う) |
https://example.com |
https://api.example.com |
×(ホストが違う) |
https://example.com:443 |
https://example.com:8443 |
×(ポートが違う) |
この「同一オリジンかどうか」の判定が、ブラウザのセキュリティ機構である**同一オリジンポリシー(Same-Origin Policy)**の基準になっています。
なぜ制限が必要なのか — Same-Origin Policyの目的
ブラウザはデフォルトで、JavaScriptが別オリジンのリソースへ自由にアクセスすることを禁止しています。これは以下のような攻撃を防ぐためです。
- ユーザーが銀行サイト(
https://bank.example.com)にログイン中の状態で、悪意のあるサイト(https://evil.example.com)を開く - 悪意のあるサイトのJavaScriptが、ユーザーのブラウザに保存された銀行サイトのクッキー(セッション情報)を使って、裏で銀行サイトのAPIにリクエストを送る
- 何も知らないユーザーの認証情報を悪用して、不正な操作(送金など)が行われる
このような CSRF(Cross-Site Request Forgery)に類する被害を防ぐため、ブラウザは「JavaScriptから別オリジンへのリクエスト結果を読み取ること」を原則禁止しています。
ここで重要なのは、リクエスト自体は送られているという点です。CORSが防いでいるのは「レスポンスをJavaScriptが読み取れるかどうか」であり、サーバー側にリクエストが届くこと自体は止められません(後述のプリフライトを除く)。
CORSは「制限を緩和する仕組み」
CORSは制限そのものではなく、Same-Origin Policyという制限を、サーバーの許可のもとで安全に緩和する仕組みです。サーバーが「このオリジンからのアクセスは許可します」とレスポンスヘッダーで明示することで、ブラウザがレスポンスの読み取りを許可します。
サーバーが返す代表的なヘッダーは以下の通りです。
-
Access-Control-Allow-Origin: 許可するオリジン(*で全許可、または特定オリジンを指定) -
Access-Control-Allow-Methods: 許可するHTTPメソッド -
Access-Control-Allow-Headers: 許可するリクエストヘッダー -
Access-Control-Allow-Credentials: クッキーなどの資格情報を含むリクエストを許可するか
プリフライトリクエスト(Preflight)とは
GETの単純なリクエストでは上記の流れだけで完了しますが、以下のような「単純リクエストの条件を超える」リクエストの場合、ブラウザは本番のリクエストの前に**確認用のリクエスト(プリフライト)**を自動で送ります。
プリフライトが発生する代表例:
-
PUT/DELETEなどのメソッドを使う -
Content-Type: application/jsonのようなカスタムヘッダーを使う - 独自のリクエストヘッダー(
Authorizationなど)を付与する
OPTIONS メソッドで送られるこの確認リクエストに対し、サーバー側が許可するメソッド・ヘッダーを正しく返さなければ、本番のリクエストはブラウザによってブロックされます。サーバー実装側で OPTIONS への対応を忘れて「CORSエラーが消えない」というのは、初学者が踏みやすい典型的な罠です。
実際のコード例(Express.js)
サーバー側でCORSを許可する実装はフレームワークによって書き方が異なりますが、Node.jsのExpressであれば以下のように設定します。
const express = require('express');
const cors = require('cors');
const app = express();
// 特定のオリジンのみ許可する設定
app.use(cors({
origin: 'https://app.example.com',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
credentials: true,
}));
app.get('/data', (req, res) => {
res.json({ message: 'CORSを通過したリクエストです' });
});
app.listen(3000);
origin: '*' と書けば全オリジンを許可できますが、credentials: true(クッキーを使う認証)と組み合わせる場合は * を指定できず、具体的なオリジンを明示する必要があります。これはブラウザ側の仕様で定められた制約です。
筆者の考え・所感
個人的には、CORSは「フロントエンドとバックエンドが別オリジンで動くのが当たり前になった時代に、後から取り付けられたセキュリティの蝶番(ちょうつがい)」のような存在だと感じています。
実務でAPIサーバーを開発していると、開発初期に必ず一度は「CORSエラーで動かない」という壁にぶつかります。最初の頃は Access-Control-Allow-Origin: * を設定して通してしまい、それで「解決した」と思ってしまいがちでした。しかし後から振り返ると、これは制限を回避しただけで仕組みを理解したわけではなく、本番環境で認証付きAPIを * のまま運用してしまうと、意図しないオリジンからのアクセスを許してしまうリスクがあると気づきました。
CORSのエラーメッセージは一見ブラウザが「邪魔をしている」ように見えますが、実際にはサーバーの設定漏れをブラウザが教えてくれているだけです。エラーが出たら「どのオリジンからのアクセスを許可すればいいか」をサーバー側で明示的に決める、という思考に切り替えると、CORSとの付き合い方がずいぶん楽になると感じています。
また、プリフライトリクエストの存在を知らないと「OPTIONS メソッドへの謎のリクエストが来ている」と混乱することがあります。ネットワークタブで観測される通信の意味を理解しておくと、デバッグの速度が大きく変わる実感があります。
まとめ
- オリジンはスキーム・ホスト・ポートの組み合わせで決まり、1つでも違えば別オリジンとなる
- Same-Origin Policyはクッキー悪用などの攻撃を防ぐためにブラウザが標準で持つ制限であり、CORSはサーバーの許可のもとでこれを安全に緩和する仕組みである
-
PUTやカスタムヘッダーを使うリクエストでは**プリフライト(OPTIONS)**が事前に送られ、サーバーがこれに正しく応答する必要がある