別のウェブサイトに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}`); }
-
次に、関数の名前を
callback
URLパラメータとして使用して、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-Control
Content-Language
Content-Length
Content-Type
Expires
Last-Modified
Pragma
他のレスポンスヘッダーにアクセスするとエラーが発生します。
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.jp
Access-Control-Allow-Methods: PATCH
Access-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。
- ヘッダー – 設定できるのは以下のみです
Accept
Accept-Language
Content-Language
Content-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…)を使用してください。