この用語をシェア
CORSとは
CORS(Cross-Origin Resource Sharing)は、異なるオリジン間でのリソース共有を安全に実現するためのWeb標準です。Same-Origin Policy(同一オリジンポリシー)の制約を緩和し、明示的に許可されたクロスオリジンリクエストを可能にします。Modern Web開発において、API連携やマイクロサービス間通信で必須の概念です。
オリジンとは
オリジンは、プロトコル + ドメイン + ポートの組み合わせで定義されます。
オリジンの例
URL | プロトコル | ドメイン | ポート | オリジン |
---|---|---|---|---|
https://example.com/api | https | example.com | 443 | https://example.com |
http://example.com:8080/api | http | example.com | 8080 | http://example.com:8080 |
https://api.example.com/v1 | https | api.example.com | 443 | https://api.example.com |
Same-Origin Policy(同一オリジンポリシー)
同一オリジンポリシーは、セキュリティ上の重要な制約です。異なるオリジンのリソースへのアクセスを制限することで、悪意のあるスクリプトからユーザーを保護します。
同一オリジンの判定
元のURL | 比較対象URL | 結果 | 理由 |
---|---|---|---|
https://example.com/page | https://example.com/api | 同一オリジン | すべて一致 |
https://example.com/page | http://example.com/api | クロスオリジン | プロトコル違い |
https://example.com/page | https://api.example.com/v1 | クロスオリジン | ドメイン違い |
https://example.com/page | https://example.com:8080/api | クロスオリジン | ポート違い |
CORSの動作メカニズム
Simple Request(シンプルリクエスト)
以下の条件を満たすリクエストは、プリフライトリクエストなしで送信されます。
Simple Requestの条件
- メソッド:GET, HEAD, POST のいずれか
- ヘッダー:Accept, Accept-Language, Content-Language, Content-Type(特定値のみ)
- Content-Type:application/x-www-form-urlencoded, multipart/form-data, text/plain
Simple Requestの例
// クライアントからのリクエスト
GET /api/users HTTP/1.1
Host: api.example.com
Origin: https://frontend.example.com
Accept: application/json
// サーバーからのレスポンス
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://frontend.example.com
Content-Type: application/json
{
"users": [{"id": 1, "name": "田中太郎"}]
}
Preflight Request(プリフライトリクエスト)
Simple Requestの条件を満たさないリクエストは、事前にOPTIONSメソッドでプリフライトリクエストを送信します。
プリフライトが必要な場合
- メソッド:PUT, DELETE, PATCH等
- カスタムヘッダー:Authorization, X-Custom-Header等
- Content-Type:application/json等
プリフライトリクエストの例
// 1. プリフライトリクエスト
OPTIONS /api/users/123 HTTP/1.1
Host: api.example.com
Origin: https://frontend.example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization
// 2. プリフライトレスポンス
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://frontend.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400
// 3. 実際のリクエスト
DELETE /api/users/123 HTTP/1.1
Host: api.example.com
Origin: https://frontend.example.com
Authorization: Bearer token123
// 4. 実際のレスポンス
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://frontend.example.com
CORSヘッダーの詳細
レスポンスヘッダー
ヘッダー | 説明 | 例 |
---|---|---|
Access-Control-Allow-Origin | リクエストを許可するオリジン | https://example.com |
Access-Control-Allow-Methods | 許可するHTTPメソッド | GET, POST, PUT, DELETE |
Access-Control-Allow-Headers | 許可するリクエストヘッダー | Authorization, Content-Type |
Access-Control-Allow-Credentials | 認証情報の送信を許可 | true |
Access-Control-Max-Age | プリフライトの結果のキャッシュ時間 | 86400 |
Access-Control-Expose-Headers | JavaScriptからアクセス可能なヘッダー | X-Total-Count |
リクエストヘッダー
ヘッダー | 説明 | 例 |
---|---|---|
Origin | リクエストの送信元オリジン | https://example.com |
Access-Control-Request-Method | 実際のリクエストで使用するメソッド | DELETE |
Access-Control-Request-Headers | 実際のリクエストで使用するヘッダー | Authorization |
CORSの実装例
Node.js(Express)
const express = require('express');
const app = express();
// CORSミドルウェアの実装
app.use((req, res, next) => {
const allowedOrigins = [
'https://frontend.example.com',
'https://app.example.com',
'http://localhost:3000' // 開発環境
];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.header('Access-Control-Allow-Origin', origin);
}
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
res.header('Access-Control-Allow-Credentials', true);
res.header('Access-Control-Max-Age', '86400');
// プリフライトリクエストの処理
if (req.method === 'OPTIONS') {
res.sendStatus(200);
} else {
next();
}
});
// corsパッケージを使用した実装
const cors = require('cors');
const corsOptions = {
origin: function (origin, callback) {
const allowedOrigins = [
'https://frontend.example.com',
'https://app.example.com'
];
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('CORS policy violation'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
};
app.use(cors(corsOptions));
// API エンドポイント
app.get('/api/users', (req, res) => {
res.json([
{ id: 1, name: '田中太郎' },
{ id: 2, name: '佐藤花子' }
]);
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Python(Flask)
from flask import Flask, jsonify, request
from flask_cors import CORS, cross_origin
app = Flask(__name__)
# CORS設定
CORS(app, resources={
r"/api/*": {
"origins": [
"https://frontend.example.com",
"https://app.example.com"
],
"methods": ["GET", "POST", "PUT", "DELETE"],
"allow_headers": ["Content-Type", "Authorization"]
}
})
# 個別のエンドポイントにCORSを適用
@app.route('/api/users', methods=['GET'])
@cross_origin(origins=['https://frontend.example.com'])
def get_users():
return jsonify([
{'id': 1, 'name': '田中太郎'},
{'id': 2, 'name': '佐藤花子'}
])
# カスタムCORSミドルウェア
@app.after_request
def after_request(response):
origin = request.headers.get('Origin')
allowed_origins = [
'https://frontend.example.com',
'https://app.example.com'
]
if origin in allowed_origins:
response.headers.add('Access-Control-Allow-Origin', origin)
response.headers.add('Access-Control-Allow-Headers', 'Content-Type, Authorization')
response.headers.add('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
response.headers.add('Access-Control-Allow-Credentials', 'true')
return response
if __name__ == '__main__':
app.run(debug=True)
Java(Spring Boot)
@RestController
@RequestMapping("/api")
public class UserController {
// アノテーションを使用したCORS設定
@CrossOrigin(
origins = {"https://frontend.example.com", "https://app.example.com"},
methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE},
allowedHeaders = {"Content-Type", "Authorization"},
allowCredentials = "true"
)
@GetMapping("/users")
public List getUsers() {
return Arrays.asList(
new User(1, "田中太郎"),
new User(2, "佐藤花子")
);
}
}
// グローバルCORS設定
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://frontend.example.com", "https://app.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("Content-Type", "Authorization")
.allowCredentials(true)
.maxAge(86400);
}
}
フロントエンドでのCORS対応
JavaScript(Fetch API)
// 認証情報を含むリクエスト
fetch('https://api.example.com/users', {
method: 'GET',
credentials: 'include', // クッキーや認証情報を含める
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
}
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('CORS error:', error));
// POST リクエスト
fetch('https://api.example.com/users', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: JSON.stringify({
name: '新規ユーザー',
email: 'newuser@example.com'
})
})
.then(response => {
if (!response.ok) {
throw new Error('CORS request failed');
}
return response.json();
})
.then(data => console.log(data));
jQuery(AJAX)
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (settings.crossDomain) {
xhr.setRequestHeader('Authorization', 'Bearer ' + token);
}
}
});
$.ajax({
url: 'https://api.example.com/users',
type: 'GET',
dataType: 'json',
xhrFields: {
withCredentials: true // 認証情報を含める
},
success: function(data) {
console.log(data);
},
error: function(xhr, status, error) {
console.error('CORS error:', error);
}
});
CORSのセキュリティ考慮事項
セキュリティベストプラクティス
- オリジンの厳密な指定:ワイルドカード(*)は避ける
- 認証情報の慎重な扱い:credentialsとワイルドカードの併用禁止
- 必要最小限の権限:不要なメソッドやヘッダーは許可しない
- プリフライトのキャッシュ:適切なMax-Age設定
- Origin検証:サーバー側でのOriginヘッダー検証
よくあるセキュリティ問題
問題 | リスク | 対策 |
---|---|---|
ワイルドカード(*)の使用 | 任意のオリジンからのアクセス | 明示的なオリジン指定 |
認証情報の不適切な扱い | 認証情報の漏洩 | credentialsの適切な設定 |
過度な権限付与 | 不要なアクセス許可 | 必要最小限の権限設定 |
Origin検証の不備 | 偽装されたOriginの受け入れ | サーバー側での厳密な検証 |
CORSエラーのトラブルシューティング
よくあるエラーメッセージ
- "Access to fetch at 'URL' from origin 'ORIGIN' has been blocked by CORS policy"
- "Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource"
- "Response to preflight request doesn't pass access control check"
デバッグ方法
- ブラウザの開発者ツール:NetworkタブでCORSヘッダーを確認
- プリフライトリクエスト:OPTIONSメソッドのレスポンスを確認
- サーバーログ:CORSミドルウェアのログを確認
- curl テスト:コマンドラインでのテスト
curlを使用したテスト
# プリフライトリクエストのテスト
curl -X OPTIONS \
-H "Origin: https://frontend.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Authorization" \
-v \
https://api.example.com/users
# 実際のリクエストのテスト
curl -X GET \
-H "Origin: https://frontend.example.com" \
-H "Authorization: Bearer token123" \
-v \
https://api.example.com/users
CORSの代替手法
JSONP(JSON with Padding)
- 用途:古いブラウザでのクロスオリジン通信
- 制限:GETリクエストのみ、セキュリティリスクあり
- 現状:現在は非推奨
プロキシサーバー
- 用途:開発環境でのCORS回避
- 実装:nginx、Apache、Webpack Dev Server
- 注意:本番環境では適切なCORS設定が必要
Server-Sent Events(SSE)
- 用途:サーバーからクライアントへの一方向通信
- 特徴:CORSの制約が少ない
- 制限:一方向通信のみ
CORSのベストプラクティス
- 環境別設定:開発・ステージング・本番で異なる設定
- ホワイトリスト方式:許可するオリジンの明示的な指定
- 動的Origin検証:データベースやConfigからの動的な検証
- 適切なエラーハンドリング:CORSエラーの適切な処理
- モニタリング:CORSエラーの監視と分析
CORS対応のツール
- Browser Extension:CORS Unblock、CORS Everywhere
- Development Tools:Postman、Insomnia
- Testing Tools:curl、HTTPie
- Monitoring Tools:Sentry、LogRocket
関連技術
- CSP:Content Security Policy
- SOP:Same-Origin Policy
- CSRF:Cross-Site Request Forgery
- OAuth:認証・認可フレームワーク