はじめに
「fetch() でAPIを叩いたら、コンソールに赤字のエラーが出て何も返ってこない」
フロントエンド開発で 一度は必ずぶつかる壁、それが CORS(Cross-Origin Resource Sharing) です。
この記事では、CORSの根本にある 同一オリジンポリシー から、プリフライトリクエスト の仕組み、CORSヘッダー の設定方法まで、図解で解説します。
この記事は 「図解でわかるWeb技術の仕組み」シリーズ の第9回です。
第8回:キャッシュ戦略(ブラウザ・サーバー・CDN)を先に読むと、よりスムーズに理解できます。
1. 同一オリジンポリシーとは
「オリジン」の定義
CORSを理解するには、まず オリジン(Origin) の定義を正確に知る必要があります。
オリジンは、URLの スキーム(プロトコル)+ ホスト + ポート の3つの組み合わせで決まります。
https://example.com:443/page
│ │ │
スキーム ホスト ポート
3つが完全に一致する場合のみ「同一オリジン」 です。パスやクエリパラメータは関係ありません。
同一オリジンポリシーの役割
同一オリジンポリシー(Same-Origin Policy) とは、ブラウザが持つセキュリティ機構です。
あるオリジンから読み込まれたドキュメントやスクリプトが、別のオリジンのリソースにアクセスすることを制限する
なぜこの制限が必要なのか? もし制限がなければ、以下のような攻撃が可能になります。
1. ユーザーが悪意あるサイト(https://evil.com)にアクセス
2. evil.com のJSが fetch("https://bank.example.com/api/account") を実行
3. ユーザーのCookieが自動送信され、口座情報が evil.com に漏洩
同一オリジンポリシーがあるからこそ、ステップ3でブラウザがレスポンスをブロック してくれます。
重要な誤解ポイント
同一オリジンポリシーは リクエストの送信を止めるわけではありません。リクエストはサーバーに届き、レスポンスも返ってきます。ブラウザがJavaScriptからレスポンスを読み取ることをブロック するのです。
2. CORSエラーの発生メカニズム
では、実際にCORSエラーがどのように発生するのかを見てみましょう。
よくあるCORSエラーメッセージ
Access to fetch at 'https://api.example.com/data'
from origin 'https://app.com'
has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
このエラーが出た場合、問題はフロントエンドではなく、APIサーバー側 にあります。サーバーがCORSヘッダーを返していないことが原因です。
CORSが必要になる典型的なケース
| ケース | フロントエンド | APIサーバー |
|---|---|---|
| SPA + API構成 | https://app.com |
https://api.app.com |
| ローカル開発 | http://localhost:3000 |
http://localhost:8080 |
| 外部API利用 | https://mysite.com |
https://api.openai.com |
いずれもオリジンが異なるため、CORSの設定が必要です。
3. シンプルリクエスト ― CORSヘッダーで許可する
CORSには 「シンプルリクエスト」 と 「プリフライトが必要なリクエスト」 の2種類があります。
シンプルリクエストの条件
以下の条件を すべて 満たすリクエストは「シンプルリクエスト」として扱われます。
- HTTPメソッドが
GET、HEAD、POSTのいずれか - Content-Typeが以下のいずれか
text/plainmultipart/form-dataapplication/x-www-form-urlencoded
- カスタムヘッダーを含まない(
Authorizationヘッダー等がない)
シンプルリクエストの流れ
シンプルリクエストの場合、ブラウザはそのままリクエストを送信します。サーバーが Access-Control-Allow-Origin ヘッダーをレスポンスに含めていれば、ブラウザはJavaScriptにデータを渡します。
サーバー側の設定例(Express.js)
// Express.js での CORS 設定
const express = require('express');
const app = express();
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'https://app.com');
next();
});
app.get('/data', (req, res) => {
res.json({ message: 'Hello from API' });
});
4. プリフライトリクエスト ― 本番前の「事前確認」
シンプルリクエストの条件を満たさない場合、ブラウザは プリフライトリクエスト(Preflight Request) を自動で送信します。
プリフライトが発生する条件
- HTTPメソッドが
PUT、DELETE、PATCHのいずれか -
Content-Type: application/jsonを使う -
Authorizationヘッダーを付ける - その他のカスタムヘッダーを付ける
実務では JSONでやり取りするAPIはほぼ全てプリフライトが発生 します。
プリフライトリクエストの流れ
プリフライトは2段階で動きます。
Phase 1: OPTIONS リクエスト(プリフライト)
OPTIONS /data HTTP/1.1
Host: api.example.com
Origin: https://app.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization
ブラウザが「PUT メソッドと Authorization ヘッダーを使っていい?」とサーバーに確認します。
Phase 2: サーバーの応答
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.com
Access-Control-Allow-Methods: GET, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
サーバーが「OK、許可する」と返すと、ブラウザは本番リクエストを送信します。
サーバー側の設定例(Express.js)
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'https://app.com');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.header('Access-Control-Max-Age', '86400');
// プリフライトリクエストにはすぐ返す
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
});
Access-Control-Max-Age でパフォーマンス改善
プリフライトの結果をキャッシュすることで、毎回OPTIONSリクエストが飛ぶのを防げます。86400(24時間)が一般的です。
5. CORSヘッダー一覧
CORSに関わるHTTPヘッダーを一覧で整理します。
特に重要な3つ
| ヘッダー | 役割 | 設定のポイント |
|---|---|---|
Access-Control-Allow-Origin |
許可するオリジン |
* は手軽だがCredentials併用不可 |
Access-Control-Allow-Methods |
許可するHTTPメソッド | プリフライトで使用 |
Access-Control-Allow-Headers |
許可するリクエストヘッダー |
Authorization を忘れがち |
Cookie を送信する場合(Credentials)
フロントエンドとAPIでCookieを共有する場合は、追加設定が必要です。
フロントエンド側
fetch('https://api.example.com/data', {
credentials: 'include' // Cookieを送信
});
サーバー側
Access-Control-Allow-Origin: https://app.com # * は使えない
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: * と credentials: 'include' は併用できない
Cookieを送信する場合は、必ず具体的なオリジンを指定してください。ワイルドカードを使うとブラウザがエラーを出します。
6. 実務でよくあるCORSの解決パターン
パターン1: cors ライブラリを使う(Node.js)
const cors = require('cors');
app.use(cors({
origin: 'https://app.com',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400
}));
パターン2: Nginx でCORSヘッダーを付ける
location /api/ {
add_header Access-Control-Allow-Origin "https://app.com";
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE";
add_header Access-Control-Allow-Headers "Content-Type, Authorization";
if ($request_method = OPTIONS) {
return 204;
}
proxy_pass http://backend;
}
パターン3: 開発環境ではProxyを使う(Vite / Next.js)
開発中は フロントエンドの開発サーバーにProxy設定 を入れるのが最も簡単です。
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
};
この方法なら、ブラウザから見ると同一オリジンになるため、CORSの問題が発生しません。
7. まとめ
| 項目 | ポイント |
|---|---|
| オリジン | スキーム + ホスト + ポート の3つ。1つでも違えばクロスオリジン |
| 同一オリジンポリシー | ブラウザのセキュリティ機構。異なるオリジンへのJSからのアクセスを制限 |
| CORSエラー | サーバーが Access-Control-Allow-Origin を返していないことが原因 |
| シンプルリクエスト | GET/HEAD/POST + 特定のContent-Type + カスタムヘッダーなし |
| プリフライト | 条件を満たさない場合、ブラウザがOPTIONSリクエストで事前確認 |
| Credentials | Cookie送信時は Access-Control-Allow-Origin: * が使えない |
| 開発環境 | Proxy設定で回避するのが最も手軽 |
次回予告
次回の第10回では 「REST API 設計の基本原則」 を図解で解説します。リソース設計、エンドポイントの命名規則、ステータスコードの使い分け、そして「べからず集」まで。シリーズ最終回 です。
シリーズ目次
| # | テーマ |
|---|---|
| 1 | HTTPリクエスト/レスポンスの仕組み |
| 2 | Cookie・Session・JWTの違い |
| 3 | OAuth 2.0 の仕組み |
| 4 | DNS解決の流れ |
| 5 | HTTPS・TLS通信の仕組み |
| 6 | CDN の仕組みと役割 |
| 7 | WebSocket ― 双方向リアルタイム通信 |
| 8 | キャッシュ戦略(ブラウザ・サーバー・CDN) |
| 9 | CORS ― クロスオリジンの壁を理解する(この記事) |
| 10 | REST API 設計の基本原則 |
「この記事が役に立った」と思ったら、LGTM とストックをお願いします。
@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!




