CORS とは?
CORS(Cross-Origin Resource Sharing、オリジン間リソース共有)は、異なるオリジン(ドメイン)間でのHTTPリクエストを制御するブラウザのセキュリティ機能です。
オリジンとは?
オリジンは「プロトコル + ホスト + ポート」の組み合わせです。
| URL | オリジン |
|---|---|
https://example.com/page | https://example.com |
https://example.com:443/api | https://example.com |
http://example.com/page | http://example.com ← プロトコルが違う |
https://api.example.com/v1 | https://api.example.com ← ホストが違う |
https://example.com:8080 | https://example.com:8080 ← ポートが違う |
同一オリジンポリシー
ブラウザは、セキュリティのためにJavaScriptから異なるオリジンへのリクエストをデフォルトでブロックします。これが同一オリジンポリシーです。
フロントエンド: https://myapp.com
API: https://api.myapp.com ← 別オリジン!
fetch('https://api.myapp.com/users')
→ CORSエラー!(設定がない場合)
CORSエラーのよくあるメッセージ
パターン1: Access-Control-Allow-Origin がない
Access to fetch at 'https://api.example.com/data' from origin
'https://myapp.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
原因: サーバーがCORSヘッダーを返していない。
パターン2: オリジンが許可されていない
Access to fetch at 'https://api.example.com/data' from origin
'https://myapp.com' has been blocked by CORS policy:
The 'Access-Control-Allow-Origin' header has a value 'https://other.com'
that is not equal to the supplied origin.
原因: サーバーが別のオリジンしか許可していない。
パターン3: プリフライトリクエストが失敗
Access to fetch at 'https://api.example.com/data' from origin
'https://myapp.com' has been blocked by CORS policy:
Response to preflight request doesn't pass access control check.
原因: OPTIONSリクエスト(プリフライト)にサーバーが正しく応答していない。
CORSの仕組み
シンプルリクエスト
以下の条件をすべて満たすリクエストは、プリフライトなしで送信されます:
- メソッド: GET, HEAD, POST
- ヘッダー: Accept, Content-Type(一部), etc.
- Content-Type: text/plain, multipart/form-data, application/x-www-form-urlencoded
ブラウザ → サーバー
GET /api/users HTTP/1.1
Origin: https://myapp.com
サーバー → ブラウザ
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://myapp.com
プリフライトリクエスト
上記の条件を満たさないリクエスト(JSON送信、カスタムヘッダー等)は、事前にOPTIONSリクエストが送信されます。
1. プリフライト(OPTIONS)
ブラウザ → サーバー
OPTIONS /api/users HTTP/1.1
Origin: https://myapp.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
サーバー → ブラウザ
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
2. 実際のリクエスト
ブラウザ → サーバー
POST /api/users HTTP/1.1
Origin: https://myapp.com
Content-Type: application/json
Authorization: Bearer xxx
解決方法(バックエンド別)
Express.js
import cors from 'cors';
// すべてのオリジンを許可(開発用)
app.use(cors());
// 特定のオリジンだけ許可(本番用)
app.use(cors({
origin: ['https://myapp.com', 'https://www.myapp.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true, // Cookie を送受信する場合
maxAge: 86400, // プリフライトのキャッシュ(秒)
}));
// 動的にオリジンを許可
app.use(cors({
origin: (origin, callback) => {
const allowedOrigins = ['https://myapp.com', 'https://staging.myapp.com'];
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
}));
Hono
import { Hono } from 'hono';
import { cors } from 'hono/cors';
const app = new Hono();
app.use('*', cors({
origin: ['https://myapp.com'],
allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
allowHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400,
}));
Next.js API Routes
// app/api/users/route.ts
export async function GET(request: Request) {
const data = await getUsers();
return Response.json(data, {
headers: {
'Access-Control-Allow-Origin': 'https://myapp.com',
'Access-Control-Allow-Methods': 'GET, POST',
'Access-Control-Allow-Headers': 'Content-Type',
},
});
}
export async function OPTIONS() {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': 'https://myapp.com',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
},
});
}
Nginx
server {
location /api/ {
# CORSヘッダーを追加
add_header 'Access-Control-Allow-Origin' 'https://myapp.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
add_header 'Access-Control-Max-Age' '86400' always;
# プリフライトリクエスト
if ($request_method = 'OPTIONS') {
return 204;
}
proxy_pass http://backend;
}
}
開発環境での解決方法
Vite のプロキシ設定
開発環境ではプロキシを使ってCORSを回避できます。
// vite.config.ts
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
});
// フロントエンドのコード
// /api/users → https://api.example.com/users にプロキシされる
fetch('/api/users');
Next.js の Rewrites
// next.config.js
module.exports = {
async rewrites() {
return [
{
source: '/api/:path*',
destination: 'https://api.example.com/:path*',
},
];
},
};
credentials(Cookie)を使う場合
CookieをCORSリクエストで送受信する場合、追加の設定が必要です。
バックエンド
app.use(cors({
origin: 'https://myapp.com', // * は使えない!
credentials: true,
}));
フロントエンド
// fetch
fetch('https://api.myapp.com/profile', {
credentials: 'include', // Cookie を送信
});
// axios
axios.get('https://api.myapp.com/profile', {
withCredentials: true,
});
重要: credentials: true の場合、Access-Control-Allow-Origin に * は使えません。具体的なオリジンを指定する必要があります。
よくある間違い
1. Access-Control-Allow-Origin: * と credentials の併用
NG: Access-Control-Allow-Origin: *
credentials: true
OK: Access-Control-Allow-Origin: https://myapp.com
credentials: true
2. OPTIONSメソッドの未対応
// NG: POSTだけ対応
app.post('/api/data', handler);
// OK: OPTIONSも対応
app.options('/api/data', cors());
app.post('/api/data', handler);
// 最善: ミドルウェアで全ルートに適用
app.use(cors());
3. CORSをフロントエンドで解決しようとする
CORSはブラウザのセキュリティ機能なので、バックエンド側で設定する必要があります。フロントエンドだけでは解決できません。
セキュリティ上の注意点
Access-Control-Allow-Origin: * のリスク
// 危険: すべてのオリジンを許可
app.use(cors({ origin: '*' }));
// 安全: 特定のオリジンだけ許可
app.use(cors({ origin: ['https://myapp.com'] }));
公開APIでない限り、* は使わないようにしましょう。
リクエストのOriginヘッダーを信用しすぎない
CORSはブラウザの機能です。curl等のツールからはCORSに関係なくリクエストが送れます。CORSだけに頼らず、認証・認可も適切に実装しましょう。
デバッグ方法
ブラウザの開発者ツール
- Network タブを開く
- 失敗したリクエストを選択
- Headers タブで Response Headers を確認
Access-Control-Allow-Originが正しく設定されているか確認
curl でプリフライトをテスト
curl -X OPTIONS https://api.example.com/data \
-H "Origin: https://myapp.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type" \
-v
まとめ
| 症状 | 解決策 |
|---|---|
No 'Access-Control-Allow-Origin' | サーバーにCORSヘッダーを追加 |
| プリフライトが失敗 | OPTIONSメソッドに対応 |
| Cookie が送信されない | credentials: true + 具体的なオリジン |
| 開発環境でCORSエラー | プロキシ設定を使う |
CORSは「フロントエンド開発者なら誰もが一度はハマる」定番の問題です。仕組みを理解すれば、適切な対処ができるようになります。
APIのURLパラメータのエンコード・デコードには、AssistyのURLエンコード/デコードツールが便利です。