別のウェブサイトにfetchリクエストを送信すると、おそらく失敗します。
たとえば、http://example.comを取得してみましょう。
try {
await fetch('http://example.com');
} catch(err) {
alert(err); // Failed to fetch
}
予想通り、Fetchは失敗します。
ここでの中心となる概念は、*オリジン* - ドメイン/ポート/プロトコルのトリプレットです。
クロスオリジンリクエスト - 別のドメイン(サブドメインでも)、プロトコル、またはポートに送信されるリクエスト - には、リモート側からの特別なヘッダーが必要です。
そのポリシーは「CORS」と呼ばれます:クロスオリジンリソースシェアリング。
CORSが必要な理由:簡単な歴史
CORSは、インターネットを悪意のあるハッカーから保護するために存在します。
本当に。簡単な歴史的余談をしましょう。
長年、あるサイトのスクリプトは別のサイトのコンテンツにアクセスできませんでした。
そのシンプルでありながら強力なルールは、インターネットセキュリティの基盤でした。たとえば、ウェブサイトhacker.comの悪意のあるスクリプトは、ウェブサイトgmail.comのユーザーのメールボックスにアクセスできませんでした。人々は安全を感じました。
JavaScriptにも、当時はネットワークリクエストを実行するための特別なメソッドはありませんでした。それはウェブページを装飾するためのおもちゃの言語でした。
しかし、Web開発者はより多くの力を要求しました。制限を回避し、他のウェブサイトにリクエストを行うために、さまざまなトリックが発明されました。
フォームの使用
別のサーバーと通信する1つの方法は、そこに<form>を送信することでした。人々はそれを<iframe>に送信し、現在のページにとどまるようにしました。このように:
<!-- form target -->
<iframe name="iframe"></iframe>
<!-- a form could be dynamically generated and submitted by JavaScript -->
<form target="iframe" method="POST" action="http://another.com/…">
...
</form>
そのため、フォームはどこにでもデータを送信できるため、ネットワークメソッドがなくても、別のサイトにGET / POSTリクエストを行うことができました。ただし、別のサイトから<iframe>のコンテンツにアクセスすることは禁止されているため、レスポンスを読み取ることができませんでした。
正確に言うと、実際にはそのためのトリックがあり、iframeとページの両方に特別なスクリプトが必要でした。そのため、iframeとの通信は技術的に可能でした。今は詳細に入る意味がないので、これらの恐竜を安らかに眠らせておきましょう。
スクリプトの使用
もう1つのトリックは、scriptタグを使用することでした。スクリプトは、<script src="http://another.com/…">のように、任意のドメインを持つ任意のsrcを持つことができます。任意のウェブサイトからスクリプトを実行できます。
たとえば、another.comなどのウェブサイトがこの種のアクセスのためのデータを公開することを目的としている場合、いわゆる「JSONP(パディング付きJSON)」プロトコルが使用されました。
仕組みは次のとおりです。
たとえば、私たちのサイトで、天気などのhttp://another.comからデータを取得する必要があるとしましょう。
-
まず、事前に、データを受け入れるグローバル関数を宣言します。たとえば、
gotWeatherです。// 1. Declare the function to process the weather data function gotWeather({ temperature, humidity }) { alert(`temperature: ${temperature}, humidity: ${humidity}`); } -
次に、関数の名前を
callbackURLパラメータとして使用して、src="http://another.com/weather.json?callback=gotWeather"を持つ<script>タグを作成します。let script = document.createElement('script'); script.src = `http://another.com/weather.json?callback=gotWeather`; document.body.append(script); -
リモートサーバー
another.comは、私たちに受信させたいデータを使用してgotWeather(...)を呼び出すスクリプトを動的に生成します。// The expected answer from the server looks like this: gotWeather({ temperature: 25, humidity: 78 }); -
リモートスクリプトがロードされて実行されると、
gotWeatherが実行され、それが私たちの関数であるため、データが得られます。
これは機能し、セキュリティに違反しません。なぜなら、両方がこの方法でデータを渡すことに同意したからです。そして、両方が同意すれば、それは間違いなくハックではありません。非常に古いブラウザでも機能するため、そのようなアクセスを提供するサービスはまだあります。
しばらくして、ネットワークメソッドがブラウザのJavaScriptに登場しました。
最初は、クロスオリジンリクエストは禁止されていました。しかし、長い議論の結果、クロスオリジンリクエストは許可されましたが、新しい機能には、特別なヘッダーで表されるサーバーによる明示的な許可が必要でした。
安全なリクエスト
クロスオリジンリクエストには2つのタイプがあります。
- 安全なリクエスト。
- その他すべて。
安全なリクエストは作成が簡単なので、そこから始めましょう。
リクエストは、次の2つの条件を満たす場合に安全です。
- 安全なメソッド:GET、POST、またはHEAD
- 安全なヘッダー - 許可されるカスタムヘッダーは次のとおりです。
Accept,Accept-Language,Content-Language,- 値が
application/x-www-form-urlencoded、multipart/form-data、またはtext/plainのContent-Type。
その他のすべてのリクエストは「安全でない」と見なされます。たとえば、PUTメソッドまたはAPI-Key HTTPヘッダーを使用したリクエストは、制限に適合しません。
本質的な違いは、安全なリクエストは、特別なメソッドなしで、<form>または<script>を使用して行うことができるということです。
そのため、非常に古いサーバーでも安全なリクエストを受け入れる準備ができていなければなりません。
それとは反対に、標準以外のヘッダーや、たとえばメソッドDELETEを使用したリクエストは、この方法では作成できません。長い間、JavaScriptはこのようなリクエストを実行できませんでした。そのため、古いサーバーは、そのようなリクエストが特権ソースからのものであると想定している場合があります。「Webページはそれらを送信できないため」です。
安全でないリクエストを作成しようとすると、ブラウザは特別な「プリフライト」リクエストを送信し、サーバーにそのようなクロスオリジンリクエストを受け入れるかどうかを尋ねます。
そして、サーバーがヘッダーで明示的に確認しない限り、安全でないリクエストは送信されません。
では、詳細を見ていきましょう。
安全なリクエストのCORS
リクエストがクロスオリジンの場合、ブラウザは常にOriginヘッダーを追加します。
たとえば、https://javascriptinfo.dokyumento.jp/pageからhttps://anywhere.com/requestをリクエストする場合、ヘッダーは次のようになります。
GET /request
Host: anywhere.com
Origin: https://javascriptinfo.dokyumento.jp
...
ご覧のとおり、Originヘッダーには、パスなしで、オリジン(ドメイン/プロトコル/ポート)が正確に含まれています。
サーバーはOriginを検査し、そのようなリクエストを受け入れることに同意する場合、特別なヘッダーAccess-Control-Allow-Originをレスポンスに追加します。そのヘッダーには、許可されたオリジン(この場合はhttps://javascriptinfo.dokyumento.jp)またはスター*が含まれている必要があります。その後、レスポンスは成功し、そうでない場合はエラーになります。
ブラウザはここで信頼できる仲介者の役割を果たします。
- 正しい
Originがクロスオリジンリクエストで送信されることを保証します。 - レスポンスで許可する
Access-Control-Allow-Originがあるかどうかを確認し、存在する場合はJavaScriptがレスポンスにアクセスすることを許可し、そうでない場合はエラーで失敗します。
許可的なサーバーレスポンスの例を次に示します。
200 OK
Content-Type:text/html; charset=UTF-8
Access-Control-Allow-Origin: https://javascriptinfo.dokyumento.jp
レスポンスヘッダー
クロスオリジンリクエストの場合、デフォルトではJavaScriptはいわゆる「安全な」レスポンスヘッダーにのみアクセスできます。
Cache-ControlContent-LanguageContent-LengthContent-TypeExpiresLast-ModifiedPragma
他のレスポンスヘッダーにアクセスするとエラーが発生します。
JavaScriptに他のレスポンスヘッダーへのアクセスを許可するには、サーバーはAccess-Control-Expose-Headersヘッダーを送信する必要があります。アクセス可能にする必要がある安全でないヘッダー名のコンマ区切りリストが含まれています。
例えば
200 OK
Content-Type:text/html; charset=UTF-8
Content-Length: 12345
Content-Encoding: gzip
API-Key: 2c9de507f2c54aa1
Access-Control-Allow-Origin: https://javascriptinfo.dokyumento.jp
Access-Control-Expose-Headers: Content-Encoding,API-Key
このようなAccess-Control-Expose-Headersヘッダーを使用すると、スクリプトはレスポンスのContent-EncodingおよびAPI-Keyヘッダーを読み取ることができます。
「安全でない」リクエスト
任意のHTTPメソッドを使用できます。GET/POSTだけでなく、PATCH、DELETEなども使用できます。
少し前までは、Webページがこのようなリクエストを行うことができるとは誰も想像できませんでした。そのため、標準以外のメソッドを「これはブラウザではない」というシグナルとして扱うWebサービスがまだ存在する可能性があります。アクセス権を確認するときに、それを考慮に入れることができます。
そのため、誤解を避けるために、ブラウザは「安全でない」リクエスト(昔はできなかったリクエスト)をすぐに実行しません。最初に、許可を求めるための予備的な、いわゆる「プリフライト」リクエストを送信します。
プリフライトリクエストは、メソッドOPTIONS、本文なし、および3つのヘッダーを使用します。
Access-Control-Request-Methodヘッダーには、安全でないリクエストのメソッドがあります。Access-Control-Request-Headersヘッダーは、その安全でないHTTPヘッダーのコンマ区切りリストを提供します。Originヘッダーは、リクエストの送信元を示します。(https://javascriptinfo.dokyumento.jpなど)
サーバーがリクエストを提供することに同意する場合、空の本文、ステータス200、およびヘッダーで応答する必要があります。
Access-Control-Allow-Originは、*またはリクエスト元のオリジン(https://javascriptinfo.dokyumento.jpなど)のいずれかである必要があります。Access-Control-Allow-Methodsには、許可されたメソッドが必要です。Access-Control-Allow-Headersには、許可されたヘッダーのリストが必要です。- さらに、ヘッダー
Access-Control-Max-Ageは、権限をキャッシュする秒数を指定できます。そのため、ブラウザは、指定された権限を満たす後続のリクエストに対してプリフライトを送信する必要がなくなります。
クロスオリジンPATCHリクエストの例で、段階的に動作を確認してみましょう(このメソッドは多くの場合、データの更新に使用されます)。
let response = await fetch('https://site.com/service.json', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'API-Key': 'secret'
}
});
リクエストが安全でない理由は3つあります(1つで十分です)。
- メソッド
PATCH Content-Typeは、application/x-www-form-urlencoded、multipart/form-data、text/plainのいずれでもありません。- 「安全でない」
API-Keyヘッダー。
ステップ1(プリフライトリクエスト)
このようなリクエストを送信する前に、ブラウザは独自に、次のようなプリフライトリクエストを送信します。
OPTIONS /service.json
Host: site.com
Origin: https://javascriptinfo.dokyumento.jp
Access-Control-Request-Method: PATCH
Access-Control-Request-Headers: Content-Type,API-Key
- メソッド:
OPTIONS. - パス – メインリクエストと全く同じです:
/service.json. - クロスオリジン特有のヘッダー
Origin– ソースオリジン。Access-Control-Request-Method– リクエストされたメソッド。Access-Control-Request-Headers– カンマ区切りの「安全でない」ヘッダーのリスト。
ステップ2 (プリフライトレスポンス)
サーバーはステータス200と以下のヘッダーで応答する必要があります
Access-Control-Allow-Origin: https://javascriptinfo.dokyumento.jpAccess-Control-Allow-Methods: PATCHAccess-Control-Allow-Headers: Content-Type,API-Key.
これにより、今後の通信が許可されます。許可されない場合はエラーが発生します。
サーバーが今後他のメソッドやヘッダーを期待する場合は、それらをリストに追加して事前に許可しておくことが理にかなっています。
例えば、このレスポンスはPUT、DELETE、追加のヘッダーも許可します
200 OK
Access-Control-Allow-Origin: https://javascriptinfo.dokyumento.jp
Access-Control-Allow-Methods: PUT,PATCH,DELETE
Access-Control-Allow-Headers: API-Key,Content-Type,If-Modified-Since,Cache-Control
Access-Control-Max-Age: 86400
ブラウザは、PATCH が Access-Control-Allow-Methods に含まれており、Content-Type,API-Key が Access-Control-Allow-Headers のリストに含まれていることを確認できるため、メインリクエストを送信します。
秒数を示す Access-Control-Max-Age ヘッダーが存在する場合、プリフライトの許可は指定された時間キャッシュされます。上記のレスポンスは86400秒(1日)キャッシュされます。この期間内は、後続のリクエストでプリフライトは発生しません。キャッシュされた許可に適合する場合、リクエストは直接送信されます。
ステップ3 (実際のリクエスト)
プリフライトが成功すると、ブラウザはメインリクエストを実行します。ここでのプロセスは、安全なリクエストの場合と同じです。
メインリクエストには、Origin ヘッダーが含まれています(クロスオリジンであるため)。
PATCH /service.json
Host: site.com
Content-Type: application/json
API-Key: secret
Origin: https://javascriptinfo.dokyumento.jp
ステップ4 (実際のリクエスト)
サーバーは、メインレスポンスに Access-Control-Allow-Origin を追加することを忘れてはなりません。プリフライトが成功しても、この義務は免除されません。
Access-Control-Allow-Origin: https://javascriptinfo.dokyumento.jp
その後、JavaScriptはメインサーバーのレスポンスを読み取ることができます。
プリフライトリクエストは「バックグラウンドで」発生し、JavaScriptには見えません。
JavaScriptは、メインリクエストへのレスポンス、またはサーバーの許可がない場合はエラーのみを取得します。
クレデンシャル
JavaScriptコードによって開始されたクロスオリジンリクエストは、デフォルトではクレデンシャル(CookieまたはHTTP認証)を持ちません。
これはHTTPリクエストでは一般的ではありません。通常、http://site.comへのリクエストには、そのドメインからのすべてのCookieが添付されます。一方、JavaScriptメソッドによって行われたクロスオリジンリクエストは例外です。
例えば、fetch('http://another.com') は、another.com ドメインに属するCookieであっても (!) Cookieを送信しません。
なぜでしょうか?
これは、クレデンシャル付きのリクエストが、クレデンシャルなしのリクエストよりもはるかに強力であるためです。許可された場合、JavaScriptはユーザーに代わって行動し、ユーザーのクレデンシャルを使用して機密情報にアクセスする完全な権限を与えられます。
サーバーは本当にスクリプトをそれほど信頼しているのでしょうか?信頼している場合は、追加のヘッダーを使用して、クレデンシャル付きのリクエストを明示的に許可する必要があります。
fetch でクレデンシャルを送信するには、次のように credentials: "include" オプションを追加する必要があります
fetch('http://another.com', {
credentials: "include"
});
これで、fetch は another.com から発信されたCookieを、そのサイトへのリクエストと共に送信します。
サーバーが *クレデンシャル付き* のリクエストを受け入れることに同意する場合、Access-Control-Allow-Origin に加えて、レスポンスに Access-Control-Allow-Credentials: true ヘッダーを追加する必要があります。
例えば
200 OK
Access-Control-Allow-Origin: https://javascriptinfo.dokyumento.jp
Access-Control-Allow-Credentials: true
注意点: クレデンシャル付きのリクエストの場合、Access-Control-Allow-Origin でスター * を使用することは禁止されています。上記のように、正確なオリジンを指定する必要があります。これは、サーバーがこのようなリクエストを行うことを信頼している相手を本当に知っていることを確認するための追加の安全対策です。
まとめ
ブラウザの観点からは、クロスオリジンリクエストには「安全な」リクエストとそれ以外の2種類があります。
「安全な」リクエストは、以下の条件を満たす必要があります
- メソッド: GET、POST、または HEAD。
- ヘッダー – 設定できるのは以下のみです
AcceptAccept-LanguageContent-LanguageContent-Typeをapplication/x-www-form-urlencoded、multipart/form-data、またはtext/plainに設定します。
重要な違いは、安全なリクエストは <form> タグまたは <script> タグを使用して古くから実行可能であったのに対し、安全でないリクエストはブラウザでは長期間不可能であったことです。
そのため、実際の違いは、安全なリクエストは Origin ヘッダー付きですぐに送信されるのに対し、他のリクエストの場合はブラウザが許可を求める予備的な「プリフライト」リクエストを行うことです。
安全なリクエストの場合
- → ブラウザはオリジンと共に
Originヘッダーを送信します。 - ← クレデンシャルなしのリクエスト(デフォルトでは送信されない)の場合、サーバーは以下を設定する必要があります
Access-Control-Allow-Originを*またはOriginと同じ値に設定します
- ← クレデンシャル付きのリクエストの場合、サーバーは以下を設定する必要があります
Access-Control-Allow-OriginをOriginと同じ値に設定しますAccess-Control-Allow-Credentialsをtrueに設定します
さらに、JavaScriptに Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、または Pragma 以外のレスポンスヘッダーへのアクセスを許可するには、サーバーは許可されたヘッダーを Access-Control-Expose-Headers ヘッダーにリストする必要があります。
安全でないリクエストの場合、リクエストされたリクエストの前に予備的な「プリフライト」リクエストが発行されます
- → ブラウザは、以下のヘッダーを含む
OPTIONSリクエストを同じURLに送信しますAccess-Control-Request-Methodにはリクエストされたメソッドが含まれます。Access-Control-Request-Headersには、安全でないリクエストされたヘッダーがリストされます。
- ← サーバーは、ステータス200と以下のヘッダーで応答する必要があります
Access-Control-Allow-Methodsには許可されたメソッドのリストが含まれます。Access-Control-Allow-Headersには許可されたヘッダーのリストが含まれます。Access-Control-Max-Ageには、許可をキャッシュする秒数が含まれます。
- その後、実際のリクエストが送信され、前の「安全な」スキームが適用されます。
コメント
<code>タグを使用し、複数行の場合は<pre>タグで囲み、10行を超える場合はサンドボックス(plnkr、jsbin、codepen…)を使用してください。