CORSを理解する
CORSについての理解が浅く、業務で躓いた場面があったので、 MDN Web Docs などを読みながら理解した内容と実際にブラウザで動かしながら理解した内容について整理しました。
内容に誤りがあればご指摘いただけますと幸いです。
CORSとは
CORS (オリジン間リソース共有, Cross-Origin Resource Sharing) は、クロスオリジンリクエストを許可するための仕組みです。
前提として、ブラウザとサーバーが同一オリジン間 (例: https://domain-a.com から https://domain-a.com) のHTTPリクエストは自由に実行できます。
一方で、異なるオリジン間 (例: https://domain-a.com から https://domain-b.com) のHTTPリクエスト (クロスオリジンリクエスト) が発生した場合、通常はブラウザのセキュリティ機能によってブロックされます。
ここで異なるオリジン間のリクエストを許可するために登場するのがCORSです。
具体的には、サーバー側で特定のオリジンからのリクエストを許可するために Access-Control-
で始まるCORSヘッダーを設定します。このヘッダーにより、ブラウザは特定のオリジンからのリクエストを許可されたものとして扱うことができます。
プリフライトリクエスト
ブラウザがクロスオリジンリクエストを送信する際、プリフライトリクエストというものを実行します (後述のシンプルリクエストの場合は除く)
プリフライトリクエストは、リクエストの始めにOPTIONSメソッドのHTTPリクエストを他ドメインのリソースに対して送信し、どのようなリクエストメソッドが許可されているかを問い合わせるために使用されます。
実際にプリフライトリクエストの送信を確認してみる
以下のhtmlファイルを用意し、ローカルサーバー上でHTTPリクエストのテスト用サービス JSONPlaceholder にリクエストを送信して確認してみます。
<!DOCTYPE html>
<html>
<head>
<title>Preflight Request Test</title>
</head>
<body>
<button id="testButton">Send Request</button>
<script>
document.getElementById('testButton').addEventListener('click', function() {
const url = 'https://jsonplaceholder.typicode.com/posts'
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ key: 'value' })
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
});
</script>
</body>
</html>
Send Requestボタンを押下すると、検証 > Network を確認すると、メインリクエストの前にブラウザから自動的にプリフライトリクエストが送信されていることが確認できます。
リクエストの流れを図で表現すると以下の通りです。
図に記載したCORSヘッダーの詳細について以下で解説します。
Origin と Access-Control-Allow-Origin
リクエストの「Origin」ヘッダーは、リクエストの発信元を示します。
レスポンスの「Access-Control-Allow-Origin」ヘッダーは、サーバーがアクセスを許可するオリジンを指定します。
この場合、サーバーは http://127.0.0.1:5500 からのアクセスを許可しています。
この確認はプリフライトリクエストだけではなく、メインリクエストでも発生します。
Access-Control-Request-Method と Access-Control-Allow-Methods
リクエストの「Access-Control-Request-Method」は、実際のリクエストで使用するHTTPメソッドを示します。
レスポンスの「Access-Control-Allow-Methods」は、サーバーが許可するHTTPメソッドのリストです。
ここでは、POSTメソッドが要求され、サーバーはGET, POST, PUT, DELETE, PATCH, OPTIONSを許可しています。
Access-Control-Request-Headers と Access-Control-Allow-Headers
リクエストの「Access-Control-Request-Headers」は、実際のリクエストで使用するカスタムヘッダーを指定します。
レスポンスの「Access-Control-Allow-Headers」は、サーバーが許可するヘッダーを指定します。
この例では、content-typeヘッダーが要求され、許可されています。
その他CORSヘッダー
Access-Control-Allow-Credentials: true
クロスオリジンリクエストでクレデンシャル(Cookieなど)の送信を許可します。
Access-Control-Max-Age: 3600
プリフライトリクエストの結果をキャッシュできる時間(秒)を指定します。
Access-Control-Expose-Headers: Location
クロスオリジンリクエストで、クライアントのJavaScriptがLocationヘッダー(リダイレクト先を示したヘッダー)にアクセスすることを許可します。
シンプルリクエスト
ブラウザがクロスオリジンリクエストを送信する際、必ずしもプリフライトリクエストが実行されるわけではありません。
以下の条件を全て満たすシンプルなリクエストの場合はプリフライトリクエストなしでクロスオリジンリクエストを送信することができます。
form要素によるリクエストもこれに当たります。
- 許可されているメソッドのうちのいずれか
- GET
- HEAD
- POST
- 手動で設定できるヘッダーは以下のヘッダーだけ
- Accept
- Accept-Language
- Content-Language
- Range (単純範囲ヘッダー値、例えば bytes=256- や bytes=127-255 の場合)
- Content-Type (ヘッダーが以下のいずれかの場合)
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
- XMLHttpRequest オブジェクトを使用してリクエストを行う場合は、 XMLHttpRequest.upload プロパティから返されるオブジェクトにイベントリスナーが登録されていない
- リクエストに ReadableStream オブジェクトが使用されていない
実際にシンプルリクエストを送信してみる
先ほどのプリフライトリクエストを送信したhtmlファイルを、シンプルリクエストの条件に適用されるよう以下の内容に変更します。
<!DOCTYPE html>
<html>
<head>
<title>Preflight Request Test</title>
</head>
<body>
<button id="testButton">Send Request</button>
<script>
document.getElementById('testButton').addEventListener('click', function() {
const url = 'https://jsonplaceholder.typicode.com/posts';
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'text/plain' // application/json から変更
},
body: 'simple request', // 適当な文字列に変更
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
});
</script>
</body>
</html>
再度Send Requestボタンを押下し、検証 > Network を確認すると、 プリフライトリクエストなしでメインリクエストが送信されていることが分かります。
リクエストの流れを図で表現すると以下の通りです。
プリフライトリクエストの後に送信されたメインリクエストと違いが殆どないので、ここでは詳細な説明は割愛させていただきます。
参考
Discussion