🐷

CORSを理解する

2024/06/23に公開

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 を確認すると、 プリフライトリクエストなしでメインリクエストが送信されていることが分かります。

リクエストの流れを図で表現すると以下の通りです。

プリフライトリクエストの後に送信されたメインリクエストと違いが殆どないので、ここでは詳細な説明は割愛させていただきます。

参考

https://developer.mozilla.org/ja/docs/Web/HTTP/CORS

Discussion