📝

CORSの理解を深める

2021/09/03に公開

初めに

AngularでWEBアプリケーションを初めて書いたときに出たCORS関連のエラーを解決方法だけ検索して解決したものの、仕組みがわからないままずっと引きずっていたので調べてまとめてみます。

CORSとは

Closs Origin Resource Sharingの略
日本語で表すと『クロスオリジン間リソース共有』となります。
つまり、異なるオリジン間(クロスオリジン)でリソース共有をするためのセキュリティメカニズムです。
例えば下の図のようにdomain-a.comのWEBページ内で使用する画像を
domain-b.comから取得したい場合にCORSを使用します。

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

Originとは

あるWEBコンテンツにアクセスするために使用されるURLの
プロトコル + ホスト + ポートがそのWEBコンテンツの
オリジンとなります。
例えばcontoso.comのport=8080で動作しているWEBコンテンツのオリジンは下記のようになります。

https://contoso.com:8080

Closs Origin, Same Originとは

当たり前ですが、二つのOriginが同一ではない場合クロスオリジンとなります。
二つのOriginが同一の場合は同一オリジンとなります。
同一性の定義は、比較するオリジンのプロトコル、ホスト、ポートが全て一致するかどうかです。


以下に同一オリジンの例とクロスオリジンの例を挙げます。

オリジン比較の例

オリジン 同一? 解説
http://contoso.com/v1/index.html
http://contoso.com/v2/index.html
プロトコルとホストが同一なので同一オリジンです。
v1とv2でパスが異なっていますが、同一性にパスは含まれません。
http://contoso.com:80
http://contoso.com
サーバーは既定で80番ポートで HTTP コンテンツを配信するため、同一オリジンです。
http://contoso.com/app1
https://contoso.com/app2
× httpとhttpsで使用するプロトコルが異なるためクロスオリジンです。
http://contoso.com
http://www.contoso.com
× ホスト名が異なるためクロスオリジンです。
http://contoso.com
http://contoso.com:8080
× ポートが異なるのでクロスオリジンです。

リクエストの制限

通常、ブラウザーはセキュリティの観点からアプリケーションが読み込まれたオリジンへのリクエストのみ許可します。
クロスオリジンへのリクエストはデフォルトで許可されていません。
この仕組みをSame Origin Policy(同一オリジンポリシー)と言います。
クロスオリジンへのリクエストの場合はリクエストのヘッダにCORSを使用するための設定がされている必要があります。

Simple Requests(単純リクエスト)とは

送信するリクエストが以下の全ての条件を満たす場合は、単純リクエストとなり、
後述するプリフライトリクエストが発生しません。

  • リクエストのHTTPメソッドがGET,HEAD,POSTのいずれかである。
  • 以下のヘッダのみが設定されている。
    • Connection
    • User-Agent
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type(但し、下記の条件を満たすもの)
  • Content-Typeヘッダーが下記のいずれかである。
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
  • リクエストに使用されるどの XMLHttpRequestUpload にもイベントリスナーが登録されていないこと
  • リクエストに ReadableStream オブジェクトが使用されていないこと

Preflighted Requests(プリフライトリクエスト)とは

実際のリクエストの送信前にOPTIONSメソッドによるHTTPリクエストをクロスオリジンに向けて送信し、
実際のリクエストを送信しても安全かどうかを確かめる仕組みです。
上記の単純リクエストの条件を満たさないリクエストの場合は全てプリフライトリクエストが飛びます。

クロスオリジンリクエストの流れ

例としてexample.comからcontoso.comにリクエストを行う場合を考えてみます。

単純リクエストの場合

ヘッダは下記のようになるかと思います。
Originというヘッダがありますがこれはクロスオリジンリクエストが発生した場合、自動でブラウザが追加します。

GET /app HTTP/1.1 
Host: contoso.com
Origin: https://example.com

アクセスが許可されていた場合、contoso.comからのレスポンスは下記のようになります。

200 OK
Content-Type:text/html; charset=UTF-8
Access-Control-Allow-Origin: https://example.com

ブラウザはcontoso.comからのレスポンスのAccess-Control-Allow-Originを確認し、
アクセス元(example.com)が含まれていれば成功、そうでなければエラーとします。

プリフライトリクエストの場合

実際にプリフライトリクエストが発生する例を見てみます。

const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://bar.other/resources/post-here/');
xhr.setRequestHeader('X-PINGOTHER', 'pingpong');
xhr.setRequestHeader('Content-Type', 'application/xml');
xhr.onreadystatechange = handler;
xhr.send('<person><name>Arun</name></person>');

この例の場合、下記の理由からプリフライトリクエストが飛びます。

  • リクエストヘッダーにカスタムヘッダが設定されている(X-PINGOTHER)
  • Content-Typeヘッダーが次のいずれでもない
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain

プリフライトリクエストからの処理の流れは下記の図のような形になります。

1回目のリクエスト

  • Access-Control-Request-Methodで実際のリクエストでPOSTメソッドを使用することをサーバーに通知しています。
  • Access-Control-Request-Headersで実際のリクエストで使用するヘッダーをサーバーに通知しています。

1回目のレスポンス

  • Access-Control-Request-MethodでPOSTメソッドが許可されていることをクライアントに通知しています。
  • Access-Control-Request-Headersでリクエストヘッダーが許可されていることをクライアントに通知しています。

2回目のリクエスト

ここが実際に送信されるリクエストです。1回目のレスポンスで許可されていることを確認したメソッド、ヘッダーでリクエストを送信しています。

2回目のレスポンス

サーバーからの実際のレスポンスです。ここがクライアントが受け取りたい実際のレスポンスになります。

CORSで資格情報を取り扱う

デフォルトではxhrやfetchはクロスオリジンへのリクエストにcockieを送信しませんが
XMLHttpRequestオブジェクト、またはRequestオブジェクトのコンストラクタに特定のフラグを設定することで送信が可能になります。

const invocation = new XMLHttpRequest();
const url = 'http://bar.other/resources/credentialed-content/';

function callOtherDomain() {
  if (invocation) {
    invocation.open('GET', url, true);
    invocation.withCredentials = true; //ココ!
    invocation.onreadystatechange = handler;
    invocation.send();
  }
}

この場合、単純リクエストの条件に収まりますのでプリフライトリクエストは発行されません。
サーバーからのレスポンスにAccess-Control-Allow-Credentials: trueが含まれていない場合、
ブラウザがこのレスポンスを拒否し、WEBアプリケーションでは使用できなくなります。
また、レスポンスのAccess-Control-Allow-Originがワイルドカードで指定されている場合もリクエストが失敗します。
なので、サーバーは実際のオリジンを指定してレスポンスを作成する必要があります。

まとめ

  • CORSとは異なるオリジン間でのリソース共有をするためのセキュリティメカニズムである
  • Originはプロトコル + ホスト + ポートで表される
  • Simple Requestとは特定の条件下を満たしており、プリフライトリクエストが行われないリクエストのこと
  • Preflight RequestとはSimple Requestではないリクエストにおいて実際のリクエストの前に送信されるリクエストのこと

参考

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

GitHubで編集を提案

Discussion