クロスサイト HTTP リクエストに Web.config で対応する

SERVER


異なるドメインに置いたリソースへ Ajax でアクセスできるようにする

たとえば "ez-net.jp" ドメインにある Web ページから "api.ez-net.jp" ドメインにある XML リソースを Ajax を使って取得したい場合があります。

異なるドメインに対して Ajax で HTTP リクエストを送信すると、たとえば Google Chrome の場合は次のようなエラーが発生してリクエストが拒否されます。

XMLHttpRequest cannot load http://api.ez-net.jp/resource.xml. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://ez-net.jp' is therefor not allowed access.

このような場合は、別のドメインからアクセスされるリソースのあるサーバーで、リクエスト(要求)があったときに 'Access-Control-Allow-Origin' などのヘッダー情報を付けてレスポンス(応答)を返す必要があります。

ここでは、Microsoft Azure Web Service などで使われる IIS という Web サーバーの設定ファイル "Web.config" を使って、レスポンスに必要なヘッダー情報を付ける方法を紹介します。

 

Cross-Origin Resource Sharing (CORS) について

異なるドメインのリソースに Ajax でアクセスしたときに、そのリソースを呼び出し元のドメインが利用できるかを制御するのに使われる仕組みを Cross-Origin Resource Sharing といい、XHR2 対応のブラウザーであれば、XMLHttpRequest を使ってリクエストを送信するときに自動的に使用されます。

まずは、この CORS で行われるやり取りについて、簡単に説明しておきます。

CORS の概要

CORS では、サーバーからリソースを取得するときに、サーバーが返すレスポンスヘッダーを使って、そのリソースを読み込んでも安全(想定された読み込み元である)かを判断します。

また、リクエストの際に独自のヘッダーを "Access-Control-Request-Headers" に設定して送ることで、リソースを取得する前に OPTIONS メソッドによる通信を行い、より厳密な安全性の判断も行えるようです。この、事前にリソースが取得できるかを調べるために送信される OPTIONS リクエストのことをプリフライトリクエストと言います。

ちなみに、prototype.js (1.7.1) の Ajax.Request を使って異なるドメインのリソースを取得しようとしたときは、独自のヘッダー "X-Prototype-Version" と "X-Requested-With" を追加して通信が行われるため、プリフライトリクエストが発生します。

 

リクエスト側では JavaScript で普通にクロスサイト HTTP リクエストを行えば、XHR2 対応ブラウザーであれば、必要なヘッダーの付加やプリフライトリクエストの送信などを適切に処理してくれるので、何か難しいことをする必要はありません。

CORS のやり取りを成立させる上で必要な作業としては、別のドメインからアクセスされるリソースを置いている Web サーバーでの設定が主になります。

 

なお、この CORS という仕組みは、ブラウザーがそのリソースを扱っても問題ないかを知るための仕組みに過ぎないようなので、これで想定外の場所からのアクセスをサーバー側で禁止できるものではないことを念頭に置いて使う必要がありそうです。

プリフライトリクエストで使われるリクエストヘッダー

プリフライトリクエストが行われる場合、GET メソッドまたは POST メソッドで実際のリソースを取得する前に、まず OPTIONS メソッドを使ってリソースが取得できるかを判断します。

具体的には、リソースをリクエストする際に、次のリクエストヘッダーを使って、どのようにリソースを取得しようとしているかをサーバーに伝えています。

Origin 送信元のサーバーを伝えます。パス情報を含まない "http://ez-net.jp" といった文字列で指定します。ローカルファイルの場合は "null" を指定します。送信元を制限しない場合は "*" を指定します。プリフライトリクエストを使わないで直接取得する場合にも送信します。
Access-Control-Request-Method リソースを取得するときに使う HTTP メソッドを伝えます。たとえば "POST" などを指定します。
Access-Control-Request-Headers リソースを取得するときに使う HTTP ヘッダーを伝えます。複数のヘッダーを使う場合は、カンマ区切りのリストで指定します。

サーバーで返す必要のあるレスポンスヘッダー

レスポンス側(サーバー側)は適切なレスポンスを返してあげないと、リクエスト側がリソースを正しく取得してくれないため、クロスサイト HTTP リクエストを行うためには気を遣わないといけないところです。

レスポンス側では、GET メソッドや POST メソッドによるリソースへの実際のアクセスや、その事前の OPTIONS メソッドによるプリフライトリクエストのときに、次のようなレスポンスヘッダーを返すようにします。そうすることで、どのような状況下へそのリソースを提供することを想定しているかを、リクエスト側に伝えることができます。

Access-Control-Allow-Origin ここで示された送信元を許可することを伝えます。
Access-Control-Allow-Method リソースへのアクセスに使用できる HTTP メソッドを伝えます。"GET" や "POST" を指定します。複数のメソッドを許可する場合は、カンマ区切りで指定します。なお、プリフライトリクエストで使う "OPTIONS" メソッドは、ここで指定する必要はないようです。
Access-Control-Request-Headers リソースへのアクセスに使用できる HTTP リクエストヘッダーを伝えます。リクエスト時に独自ヘッダーが付けられた場合は、ここでそのヘッダーを明示的に許可する必要があるようです。
Access-Control-Max-Age プリフライトリクエストの応答の有効期限を秒単位で伝えます。これを伝えることで、その期間内のプリフライトリクエストのやり取りが省略でき、応答時間を短縮できます。 このヘッダーを指定しない場合は、プリフライトリクエストが必要な場面で毎回送信されるようです。

このレスポンスでリソースへのアクセス許可が確認できると、リクエスト側はそれを使って Web コンテンツの表示を行います。プリフライトリクエストの段階の場合は改めて、予定していた HTTP メソッドを使ってリソースの取得が行われます。

 

Web.config を使って CORS に必要なレスポンスを返す

IIS 上の Web ページで任意のレスポンスヘッダーを付ける方法は ASP.NET の "Response.Headers.Add()" メソッドを使うのが一般的ですが、ASP.NET の cshtml などでこのメソッドを使っても、プリフライトリクエスト時の OPTIONS では、ASP.NET スクリプトは実行されない様子です。

WebService.Attributes.ActionFilterAttribute クラスを使った実装方法もあるようでしたが、今回は簡単に Web.config を使って対応する方法でやってみます。

Web.config を設定する

Web.config で任意のレスポンスヘッダーを返すには <system.webServer> タグ内の <httpProtocol> タグの要素として <customHeaders> タグを記載します。

<system.webServer>

<httpProtocol>

<customHeaders>

<add name="Access-Control-Allow-Origin" value="http://ez-net.jp"/>

<add name="Access-Control-Allow-Methods" value="POST"/>

<add name="Access-Control-Expose-Headers" value="x-json"/>

<add name="Access-Control-Allow-Headers" value="x-prototype-version, x-requested-with"/>

<add name="Access-Control-Max-Age" value="604800"/>

</customHeaders>

</httpProtocol>

</system.webServer>

このように、レスポンスに付けて返したいヘッダーを <add> タグを使って、ヘッダー名とその値をそれぞれ "name" 属性と "value" 属性で設定します。

今回は prototype.js を使ったアクセスを想定して "Access-Control-Allow-Headers" ヘッダーに "x-prototype-version" と "x-requested-with" を設定しています。prototype.js ではクロスサイト HTTP リクエスト時にこれらのヘッダーを付けるため、このようにして許可しておく必要があります。

また、prototype.js からのリクエストに対して毎回プリフライトリクエストを行うのももったいないので、"Access-Control-Max-Age" ヘッダーで 604800 秒(7 日間)は

 

また、prototype.js で通信を行ってみると、Google Chrome では "Refused to get unsafe header "X-JSON" というメッセージがコンソールに表示されたので、"Access-Control-Expose-Headers" ヘッダーに "x-json" を設定しました。この "Access-Control-Expose-Headers" ヘッダーは、ブラウザーがアクセスできるヘッダーのホワイトリストを指定するのに使用するそうです。

ちなみに "x-json" ヘッダーというのは、prototype.js がレスポンスでこのヘッダーを受け取ったときに、その値を JavaScript 内ですぐに使えるように準備してくれる仕組みだそうです。

JavaScript 側から見た CORS の動きについての補足

このように Web.config を設定すると、このサーバーからのすべてのレスポンスに対して、これらのヘッダーが付けられます。これを使って適切な情報を返すことで、クロスサイト HTTP リクエストをエラーなく行えるようになります。

なお、この CORS にアクセスが拒否された場合でも、Ajax.Request で処理知った結果は onSuccess で受け取ることになるようです。

たとえば "application/xml" を返すレスポンスで onSuccess で指定したメソッドが呼び出されたとしても、CORS で拒否と判断された場合は JavaScript 上では responseXML は null になります。この動作は OPTIONS で拒否と判断された場合でも、直接 GET などで取得しようとして拒否と判断された場合でも同じです。

直接 GET で取得した場合は、通信上ではサーバーから XML データを取得できてはいるのですが、それが許可されていないものと判断すると、JavaScript 上では null として扱う安全策が採られている様子でした。