CORSの動作をクロスサイトでXMLHttpRequestして確認した

前回、こちらのエントリーにて、XMLHttpRequestが SOP の対象となった場合の動作を確認しました。

takapi86.hatenablog.com

今回は、Cross-Origin Resource Sharing (CORS)の仕様したがって、SOPで制限されている クロスオリジンへの XMLHttpRequest(XHR) を可能にします。

CORSについては以下を参照すると良いでしょう。

リクエストは2パターン

リクエストは2パターンあります。条件によってそれぞれ使い分けされます。

  • シンプルなリクエス
  • プリフライトリクエス

それぞれ動作を確認していきます。

シンプルなリクエス

シンプルなリクエストとは

シンプルなリクエストとは、以下のすべての条件に合うものです。

HTTP アクセス制御 (CORS)から抜粋

  • 許可されたメソッドは以下に限る:

    • 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

実際にやってみる

前回の記事で、「異なるオリジンにXHRをしてみる」というセクションで、異なるオリジンにXHRをしSOP の制御対象となることを確認しましたが、このリクエストを許可するようにします。

前回と同じ環境で、異なるオリジンのXHRが許可されるような条件を加え試します。

前回の環境

  • XHR送信側サーバ(Sinatra): http://test.example.com:4567/ (HTML,JSをレンダリングし、そこからXHRを送信します。)
  • XHR受信側サーバ(Sinatra): http://test.example.com:4568/
  • リクエストメソッド: POST
  • レスポンス: Failed to load http://test.example.com:4568/: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://test.example.com:4567' is therefore not allowed access. というエラーがブラウザに表示され、Response が受け取れない
Accept:*/*
Accept-Encoding:gzip, deflate, br
Accept-Language:ja
Connection:keep-alive
Content-Length:0
Content-Type:text/plain
Host:localhost:4567
Origin:http://test.example.com:4567
Referer:http://test.example.com:4567/tools
User-Agent:Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36

加える条件

XHR受信側サーバ(http://test.example.com:4568/) の Response Header に以下を返すようにします。

Access-Control-Allow-Origin: http://test.example.com:4567/

結果

レスポンスを受け取ることができました。

レスポンスヘッダ

Access-Control-Allow-Origin:http://test.example.com:4567
Connection:keep-alive
Content-Length:116
Content-Type:text/html;charset=utf-8
Server:thin
X-Content-Type-Options:nosniff
X-Frame-Options:SAMEORIGIN
X-XSS-Protection:1; mode=block

レスポンスボディ(レスポンスメソッド とUserAgentを返すようにしています。)

POST from [Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36]

Access-Control-Allow-Origin ヘッダ

上記の動きで確認した通り、Access-Control-Allow-Origin は、指定した Origin のアクセスを許可することができます。 リクエストで送られたOrigin ヘッダの内容とこの Access-Control-Allow-Originが同じであればアクセス許可されるという仕組みです。 (Origin ヘッダは、XHRで異なるOriginへ送信するときに自動的に付与されます。) どの Origin でもアクセスを許可させる場合は、Access-Control-Allow-Origin: * という形にもできます。 ただし、これは、後ほど説明する クレデンシャルを含むリクエスト には使うことができません。

プリフライトリクエス

以下のようなリクエストのときに、プリフライトリクエストを行います。

  • GET、HEAD、POST 以外のメソッドを使用した場合。また application/x-www-form-urlencoded、multipart/form-data、または text/plain 以外の Content-Type とともに POST を使用してリクエストを行う場合、例えば application/xml または text/xml を使用して POST で XMLペイロードをサーバーへ送るときは、リクエストでプリフライトを行います。

  • カスタムヘッダをリクエストに設定した場合 (例えば、X-PINGOTHER のようなヘッダを用いるリクエスト)。

実際にやってみる

ひとまず、ダメ元で、先ほどシンプルなリクエストで行ったもののリクエストメソッドをPOSTからPUTに変更しただけの状態で試してみます。

デベロッパーツールのConsoleには、以下のようなエラーが表示されました。

xhr.js:18 OPTIONS http://test.example.com:4568/ net::ERR_ABORTED
send_request @ xhr.js:18
onclick @ tools:22
tools:1 Failed to load http://test.example.com:4568/: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://test.example.com:4567' is therefore not allowed access. The response had HTTP status code 404.
xhr.js:18 XHR failed loading: OPTIONS "http://test.example.com:4568/".

OPTIONSメソッドでアクセスしようとしたが、サーバ側でOPTIONSを受け取る処理を行っていなかったので、404で返ってきているみたいですが、 なぜ、PUTではなく、OPTIONSで送信されたのでしょう。

プリフライトリクエストの流れ

OPTIONSで送信されたのには、以下の理由があります。

HTTP アクセス制御 (CORS)には、こう記述されています。

シンプルなリクエスト (前述) とは異なり、"プリフライト" リクエストは始めに、実際のリクエストを送信しても安全かを確かめるために他ドメインのリソースへ向けて OPTIONS メソッドを使用して HTTP リクエストを送信します。クロスサイトリクエストはユーザーデータに影響を与える可能性があるため、このようにプリフライトを行います。

つまり、流れとしては、

  1. [クライアント]OPTIONSメソッドを送信
  2. [サーバ]許可しているメソッドやカスタムヘッダをResponse Headerに含め返します。
  3. メソッドを許可: Access-Control-Request-Method
  4. カスタムヘッダを許可: Access-Control-Request-Headers
  5. 上記は後ほど詳しく説明します。

  6. [クライアント]元々送ろうとしていたリクエストを送信する

・・・あとは、通常通り

改めて実際にやってみる

今回は、PUTメソッドを許可するので、サーバ側に以下の設定を加えます。

  • OPTIONSメソッドを受け取るようにする
  • OPTIONSメソッドを受け取ったら、以下のヘッダ・値を返すようにする
    • Access-Control-Allow-Origin: http://test.example.com:4567
    • Access-Control-Allow-Methods: PUT

そして送信!

デベロッパーツールのConsoleには、以下のように、OPTIONSとPUTを送信している様子が見えます。

xhr.js:18 XHR finished loading: OPTIONS "http://test.example.com:4568/".
send_request @ xhr.js:18
onclick @ tools:22
XHR finished loading: PUT "http://test.example.com:4568/".

リクエストヘッダ(OPTIONS)

OPTIONS / HTTP/1.1
Host: localhost:4568
Connection: keep-alive
Access-Control-Request-Method: PUT
Origin: http://test.example.com:4567
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36
Accept: */*
Referer: http://test.example.com:4567/tools
Accept-Encoding: gzip, deflate, br
Accept-Language: ja,en-US;q=0.8,en;q=0.6

レスポンスヘッダ(OPTIONS)

HTTP/1.1 200 OK
Content-Type: text/html;charset=utf-8
Access-Control-Allow-Origin: http://test.example.com:4567
Access-Control-Allow-Methods: PUT
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Connection: close
Server: thin

リクエストヘッダ(PUT)

PUT / HTTP/1.1
Host: localhost:4568
Connection: keep-alive
Content-Length: 0
Accept: */*
Origin: http://test.example.com:4567
Accept-Language: ja
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36
Content-Type: text/plain
Referer: http://test.example.com:4567/tools
Accept-Encoding: gzip, deflate, br

レスポンスヘッダ(PUT)

HTTP/1.1 200 OK
Content-Type: text/html;charset=utf-8
Access-Control-Allow-Origin: http://test.example.com:4567
Content-Length: 115
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Connection: keep-alive
Server: thin

うまくリクエストを送ることができました。

クレデンシャルを含むリクエス

同一オリジンであれば、クレデンシャルを含むリクエストはデフォルトで許可されますが、 異なるオリジンへクレデンシャルを含むリクエストを送るときは、注意が必要です。

ちなみに、クレデンシャルというのは、Cookieの送信、BASIC認証を指します。

実際にやってみる

ひとまず、そのままではできないことを確認しましょう。

こんな感じで、XHRでCookieを含めて送信するようにします。

var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

デベロッパーツールのConsoleには、以下のようなエラーが表示されました。

tools:1 Failed to load http://test.example.com:4568/: The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'. Origin 'http://test.example.com:4567' is therefore not allowed access. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
xhr.js:19 XHR failed loading: POST "http://test.example.com:4568/".

なるほど、Access-Control-Allow-Credentials ヘッダを追加というものをつけて上げる必要があると

というわけで、サーバに以下のヘッダを返すようにします。

Access-Control-Allow-Credentials:true

注意点としては、上記を追加する場合は、Access-Control-Allow-Origin:*の指定ができません。 必ず、オリジンを指定する必要があります。

改めて実際にやってみる

では、引き続きやってみましょう。

シンプルなリクエスト・プリフライトリクエストが必要なものどちらも行いましたが、今度は、成功しました。

デベロッパーツールのConsole

xhr.js:19 XHR finished loading: POST "http://test.example.com:4568/".
send_request @ xhr.js:19
onclick @ tools:21
xhr.js:19 XHR finished loading: OPTIONS "http://test.example.com:4568/".
send_request @ xhr.js:19
onclick @ tools:22
XHR finished loading: PUT "http://test.example.com:4568/".

CORSのヘッダについて

CORSのリクエストヘッダ/レスポンスヘッダの詳細については、HTTP アクセス制御 (CORS)に詳しく書いてあるので、ここを読めばバッチリでしょう。

参考