CORSの基礎知識と解決策について
CORS(Cross-Origin Resource Sharing)とは?
CORSは、Webブラウザによって制御されるHTTPベースのセキュリティメカニズムです。クライアントが自分自身以外のオリジン(Cross-Origin)からのリソースを要求できるようにします。これは、同一オリジンポリシー(SOP {Same Origin Policy})に応答するために設計されました。
すべての一般的なWebブラウザに実装されており、2014年1月にW3C推薦として承認されました。
要約すると、クロスオリジンのリソースに対応するための技術だと捉えてください。
同一オリジンポリシーに応答するために設計されたとはどういうことでしょうか、
CORSを理解するために、同一オリジンポリシーがなぜ必要なのかを見ていきましょう。
同一オリジンポリシー(SOP)
スキーム(プロトコル)、ホスト、ポートの3つの組み合わせをオリジンと呼び、これらが同じ場合は同一のオリジンとみなし、同じ保護範囲のリソースとして取り扱うというものです。
https://example.com/explain_cors というURLがあるとするとオリジンは以下の箇所を指します。
オリジン:https://example.com
- スキーム(プロトコル):https
- ホスト:example.com
- ポート:80
あるサイトのJavaScriptなどのクライアントスクリプトから別のサイトへのアクセスを禁止するためのセキュリティ上の制限と考えてください
iframeで埋め込んだページでJavaScriptアクセスができるとログイン情報などの秘匿情報が取得できてしまう可能性がります。
埋め込んだのが自分のサイトなら良いが、外部のサイトで勝手に埋め込まれてJavaScriptで情報を抜き取られたらインシデントになりますね。
そのセキュリティを担保するためにブラウザは同一オリジンポリシーを採用しています。
実際に同一オリジンポリシーがブラウザで効くのか検証してみます。
サイト:http://localhost:3000 から同一オリジンのhttp://localhost:3000/api にリクエストを送信してみます。
リクエストが正常に処理されていますね。
それでは別のオリジンであるhttp://site.localhost:3000/apiにリクエストを送信してみましょう。
CORSポリシーによってブロックしたと返ってきました。同一オリジンポリシーが効いていることが確認できますね。
Access to fetch at '[http://site.localhost:3000/api](http://site.localhost:3000/api)' from origin '[http://localhost:3000](http://localhost:3000/)' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
とはいってもクロスオリジンを使いたいケースは存在しますよね?
実際に私が遭遇したケースを紹介します。
サイトAに対してユニークなIDを提供するAPIを作成する必要がありました。
- サイトA:https://example.com
- IDを提供するAPI:https://api.example.com
処理の流れとしては、サイトAでjsが実行され、IDを提供するAPIにリクエストが発生します。APIは生成したデータをサイトAに返却します。
しかしこのケースではオリジンが異なるため、同一オリジンポリシーに反しているためリクエストを処理することができません。
このようなケースに対応するためにCORSは必要になってきました。
ここまででCORSが必要になった経緯について見て来たので、本題に入ろうかと思います。
CORSはどのように機能するか
CORSのリクエストは2種類に大別されます。
- Simple Request(シンプルリクエスト)
- Prefilight Request(プリフライトリクエスト)
リクエストによって動作が異なるのですが、なぜこのような違いがあるのでしょうか?
まず、どちらのリクエストとして処理するかはブラウザ側が自動的に決定します。ブラウザはどちらのリクエストにするかを以下にして決定しているかというと、リクエストが特定の条件を満たしていない場合にプリフライトリクエストとして処理することを決定します。
特定の条件(シンプルリクエストになる場合)を見ていきましょう。
シンプルリクエスト
以下の条件の場合にシンプルリクエストだとブラウザが決定します。
基本的にトークンなどのセンシティブな情報が含まれている可能性が低い場合が該当します
- HTTPメソッドがGET,HEAD,POSTのいずれか
- ヘッダーに含まれるのが以下の情報のみ
- Accept
- Accept-Language
- Content-Language
- Content-Type
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
シンプルリクエストの場合のCORS対応
CORS対応をしていない場合はでもブラウザからサーバー側へのリクエストは行われます。
サーバー側へのリクエストは正常に処理が終了しますが、クロスドメインであるため、ブラウザではレスポンスを返すことなくエラーになります。
この対応方法としてはサーバー側でどのドメインからのアクセスの場合は許可するかを設定する必要があります。
Access-Control-Allow-Origin
を設定することで解決します。
サイトA:https://example.com からIDを提供するAPI:https://api.example.com にリクエストを送る場合ではサーバー側であるAPIで以下の設定をしてあげる必要があります。
Access-Control-Allow-Origin: https://example.com
このようにすることでドメインが一致する場合は正常なレスポンスと判断し、ブラウザでのCORSエラーになることはありません。
プリフライトリクエスト
前述したようなシンプルリクエストの特定の場合に該当しない場合(リスクがあるとブラウザ側が判断した場合)にプリフライトリクエストになります。
プリフライトリクエストではブラウザがサーバーにリクエストを送信すると、最初にHTTPオプションリクエストを送信します。このOPTIONメソッドリクエストがプリフライトリクエストの特徴です。
本番のリクエストの前にOPTIONメソッドのリクエストを送信することで、CORS の設定を取得します。 ブラウザは、プリフライトリクエストの結果を基に、本番のリクエストの送信を行うかを判断します。 そのため、単純リクエストのようにサーバー側の処理が実行される心配はありません。
これが単純リクエストとプリフライトリクエストの大きな違いになります。
プリフライトリクエストの場合のCORS対応
プリフライトリクエストに対応するためには、サーバー側でOPTIONメソッドのリクエストに対応する必要があります。
app.options('*', (req, res) => {
res.set('Access-Control-Allow-Origin', '<https://example.com>');
res.set('Access-Control-Allow-Methods', 'GET, POST');
res.set('Access-Control-Allow-Headers', 'Content-Type');
res.status(204).end();
});
これにより、リクエストに対してOPTIONメソッドのレスポンスを返し、CORSの設定を追加することができます。
Access-Control-Allow-Origin
で許可するドメインを指定しています
Access-Control-Allow-Methods
で許可するHTTPメソッド(GET, POST)を指定してします。Access-Control-Allow-Headers
では、リクエストに含まれるヘッダーの種類を指定します。
Discussion
Access-Control-Allow-Origin: https://api.example.com
という指定だと解決しないような気がしています(https://example.com)
ただの記載まちがいだとは思いますが一応......!
T-NEXさん
おっしゃる通りです🙏
修正しておきます!
ご指摘いただきありがとうございます!