こんばんは。今日は、オリジン間リソース共有の仕組みについて整理しておこうと思います。
今開発しているアプリケーションで、ちょうどこのCORSのエラーが発生して対処する必要があったので、その過程でCORSについて学んだことを整理しておこうと思った次第です。
それでは早速参ります!
Contents
今回の教材
今回、勉強するにあたり、以下の記事に大変お世話になりました。
Mozillaの記事
https://developer.mozilla.org/ja/docs/Web/HTTP/CORS
Qiitaの記事
https://qiita.com/att55/items/2154a8aad8bf1409db2b
CORSの概要
CORSの概要は以下の通りです。
- 追加の HTTP ヘッダーを使用して、あるオリジンで動作しているウェブアプリケーションに、異なるオリジンにある選択されたリソースへのアクセス権を与えるようブラウザーに指示するための仕組み
- オリジンとは、https://domain-a.com:443のようにプロトコル+ドメイン+ポートの組み合わせ
- XSS (クロスサイトスクリプティング)やCSRF(クロスサイトリクエストフォージェリー)といった悪意のある攻撃からアプリケーション・ユーザを守るために異なるオリジン間でのリソース共有を制限する必要があった(Same Origin Policy)
- 2004年に提唱されて、2014にW3C Recommendationとして承認された。
- CORSを実現するための方式として、Simple RequestとPreflight Requestの2パターンがある。
上記のように、domain-a.com上のアプリケーション・サーバ間の通信は、同じドメイン間なので許可されるが、domain-a.comのアプリケーションからdomain-b.comのサーバへの通信はブラウザの規定では許可されない。
これを、特別なヘッダを付与することで可能とする仕組みをCORSと言います。
CORSを実現するリクエストのフロー
CORSにはSimple RequestとPreflight Requestのパターンがあると書きましたが、どのような場合にどちらのパターンになるかは、以下によって決まります。
以下の全てに当てはまる場合のみ、Simple Requestになります。それ以外の場合は、サーバに悪影響を及ぼす可能性のあるリクエストとみなされて、Preflight Requestとなります。
- 以下メソッドのうちの一つであること。
GET
/HEAD
/POST
- ユーザーエージェントによって自動的に設定されたヘッダーを除いて、以下ヘッダ以外が含まれていないこと。
Accept-Language
Content-Language
Content-Type
(但し、下記の要件を満たすもの)Content-Type
ヘッダーでは以下の値のみが許可されています。application/x-www-form-urlencoded
multipart/form-data
text/plain
- リクエストに使用されるどの
XMLHttpRequestUpload
にもイベントリスナーが登録されていないこと。 - リクエストに
ReadableStream
オブジェクトが使用されていないこと。
Simple Request
- クライアントからは、Originヘッダを付与して、Originを明示する
- サーバからは、Access-Control-Allow-Originヘッダで許可するオリジン情報を返す
Preflight Request
- 最初にPOSTでもGETでもなく、何もリソースを変更できない安全なOPTIONSリクエストを送る
- この時、クライアントからはOrigin, Access-Control-Request-Method, Access-Control-Request-Headersヘッダを付与する。
- Access-Control-Request-Methods/Headersで、それぞれ実際のリクエストがそのメソッド/ヘッダで送られることを、サーバ側に伝えています。
- サーバ側は、Access-Control-Allow-Origin/Methods/Headersを返す
- これらのヘッダで、受け入れ可能なOrigin/Methods/Headerがクライアントへ伝えられている
- この条件を満たしている場合に、初めてサーバへ正式なリクエストが送られる。
- この時のリクエストヘッダ、及びレスポンスヘッダはSimple Requestの時と同じ。
実験してみる
今回は、Preflight Reqeustのパターンについて実験してみました。クライアントからのリクエストヘッダに、上記一覧以外のヘッダを加えることで実現しています。
クライアント側のコード(JavaScript)
以下のように、JavaScriptでfetch APIを利用して、APIをたたきにいっています。
・・・
return await fetch (url, {
method: 'POST',
headers:{
'Host': 'XXXX',
'Ocp-Apim-Subscription-Key':'XXXXX',
'Ocp-Apim-Trace': 'true',
},
body:JSON.stringify(data)
})
このリクエストの様子を開発者ツールで見てみます。
すると・・・ちゃんとUpdateという名称でPreflight Requestと実際のPOSTリクエストの両方が確認できました!
それぞれのヘッダはどうなっているでしょうか。
はじめにOPTIONSリクエストです。上記でご紹介した仕様の通り、確かにOrigin、Access-Control-Request-Headers/Methodsヘッダが自動的に付与されている様子が確認できます。
また、サーバからのResponseヘッダにもAllow-Headers/Methodsヘッダが含まれていることが確認できます。
続いて、Preflight Request後の正式リクエストです。(長いのでスクショは断念)
こちらも、上記の理論の通り、REQUESTヘッダにはOriginが、RESPONSEヘッダニハAccess-Control-XXXXヘッダが含まれていることが確認できましたね・・・!
Request URL: https:XXXXX
Request Method: POST
Status Code: 200 OK
Remote Address: XXXXX
Referrer Policy: strict-origin-when-cross-origin
RESPONSE HEADER
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Expose-Headers: Ocp-Apim-Trace-Location,Ocp-Apim-ApiId,Ocp-Apim-OperationId,Ocp-Apim-ProductId,Ocp-Apim-SubscriptionId,Ocp-Apim-UserId
Content-Encoding: gzip
Content-Type: text/plain; charset=utf-8
Date: Wed, 01 Sep 2021 15:30:54 GMT
Ocp-Apim-ApiId: XXXX
Ocp-Apim-OperationId: XXXXX
Ocp-Apim-ProductId: XXXX
Ocp-Apim-SubscriptionId: XXXX
Ocp-Apim-Trace-Location: XXXX
Ocp-Apim-UserId: XXXX
Request-Context: appId=
Transfer-Encoding: chunked
Vary: Accept-Encoding
REQUEST HEADER
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: ja,en-US;q=0.9,en;q=0.8
Connection: keep-alive
Content-Length: 17201
Content-Type: text/plain;charset=UTF-8
Host: XXXX
Ocp-Apim-Subscription-Key: XXXX
Ocp-Apim-Trace: XXXX
Origin: http://localhost:3000
Referer: http://localhost:3000/
sec-ch-ua: "Chromium";v="92", " Not A;Brand";v="99", "Google Chrome";v="92"
sec-ch-ua-mobile: ?0
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
User-Agent: Mozilla/5.0 XXXXX
以上、CORSについて学んだことのまとめでした。
最後までご覧いただきありがとうございました!
もしこの記事がお役に立ちましたら、いいねをポチっていただけますと励みになります!
おしまい