Uncategorized

JavaScript のクロス ドメイン (Cross-Domain) 問題の回避と諸注意

REST サービス / Web Api の実践

ここでは、応用的なテーマをとりあげます。基本的な構築手順については、「Getting Started with ASP.NET Web Api」 (ASP.NET の場合)、または「REST サービスの作成」 (WCF の場合) を参照してください。

こんにちは。

今回は、JavaScript などブラウザー側 (クライアント側) からネットワーク リソースに接続する際のクロス ドメイン (cross-site, cross-domain) の問題と、その対処方法について、改めて、その考え方を以下に整理します。(よく質問を受けるため。)

 

クロス ドメイン リクエスト (Cross-Domain Requests, XDR) の問題を理解する

まずは、ご存じの方も多いと思いますが、基本的な背景から説明しておきましょう。

あるドメイン (例えば、google.com) からロードされたページ (HTML) 上から、JavaScript などの Web ブラウザー上のプログラミング コンポーネントを使って、別のドメイン上 (例えば、microsoft.com) のサービスに接続すると、Web ブラウザーがエラーや警告を表示して接続をブロックします。これが、今回 テーマとしているクロス ドメイン (cross-site, cross-domain) の問題です。

本来、HTML は、ドキュメントを扱うフォーマットである点は今更解説するまでもありませんが、単純なドキュメントを扱うだけであれば、ネットワーク上に分散されたイメージ ファイル (<img /> 要素で表現されるデータ) など、ドキュメントの構成要素をブラウザー上で集約するということは、大きな問題とはなりません。しかし、JavaScript をはじめとするプログラム コンポーネントが、ネットワーク上に分散されたリソースに接続する際は、同じ論理は通用しません。

例えば、ブラウザーには XMLHttpRequest オブジェクトが実装されていて、このオブジェクトを使うと、JavaScript によって、Web 上 (HTTP/HTTPS で公開されているサイト上) の必要なデータやファイルの内容を取得できます。
ここで、接続してくるユーザー (クライアント) を判断し、ユーザー (クライアント) に応じて動作の変わる、ある S1 というサイトが Web 上にあると仮定しましょう。例えば、amazon.com のお買い物サイトのようなケースです。あるユーザー U1 がこのサイト (S1) に接続して、そのユーザー (U1) だけが知り得る独自な情報を取得します。例えば、クレジットカード番号のような情報です。本来、この情報は、このユーザー U1 のブラウザーだけが扱うことができ、他のエンドポイント (他のユーザーのブラウザーなど) が接続しようと思っても、クッキー (Cookie) や、接続元の IP アドレス制約など、何らかのシステム的な手段によって保護されることでしょう。
ところが、前述の XMLHttpRequest オブジェクトを使って、JavaScript でこの S1 に接続する場合を想像してみてください。サイト (S1) から見ると、U1 のブラウザーから要求された内容と区別がつきません。つまり、ある悪意のあるユーザー U2 が、S1 とは異なる S2 のサイトにこうした JavaScript を埋め込んでおけば、U1 がこの S2 にアクセスした時点で、まるでユーザー U1 が S1 に接続したかのように処理できてしまいます。(まるで、ブラウザーが乗っ取られたような状況です。)

こうしたセキュリティー上の弱点から守るため、このクロス ドメインの制約が存在しています。

補足 : なお、実際の XMLHttpRequest オブジェクトは、ブラウザーのクッキー (Cookie) 情報などをそのまま渡すことはありません。(さまざまなパターンを考慮し、ちゃんと保護していますので安心してください。)
上記は、あくまでも「考え方」を理解していただくため例示しました。

では、どのようにこの制限を克服してプログラミングすれば良いでしょう ?

 

クロス ドメイン リクエスト (Cross-Domain Requests) 問題の一般的な回避手法

このクロス ドメインの問題に対応する一般的手法として、伝統的ないくつかの方法があるので簡単に紹介します。
どの方法も「呼ばれる側の承諾」が必要である (= サーバー側がそのように作られていなければ利用できない) という点がポイントであり、セキュアな手法です。

1. XDM (Cross Document Messaging)

まず、1 つ目の回避方法として、iframe と (HTML 5 の) postMessage を使った Cross Document Messaging による解決方法です。この方法は、以下のようなものです。

  1. ドメイン A からロードされたページ上に、ドメイン B の ifarme を hidden で挿入します。
  2. 親ページ (ドメイン A のページ) の JavaScript と iframe 上 (ドメイン B のページ上) の JavaScript を postMessage を使って相互通信します。
  3.  iframe 上の JavaScript から、ドメイン B 上のサービスを呼び出します。(この場合、同じドメイン同士なので、クロス ドメイン呼び出しではありません)
  4. 上記の 2 と 3 の組み合わせによって、親ページ (ドメイン A のページ) の JavaScript と、ドメイン B のサービスを連携させます。

この方法は、ブラウザー上のページ (iframe) を使用して連携するため、ページ自体をセキュアにしておくことで、Microsoft Account、Google、facebook、Twitter、mixi など、現在のインターネットで主流となっているクレームベースの認証 (SAML, OpenID など) と相性良く、セキュアに利用できます。このため、例えば、SharePoint や Lync など Office サーバー製品や、JavaScript 用の Facebook SDK など、主に認証を必要とする場合にこの方式が採用されています。(「SharePoint の Cross-domain library」、「UCWA 開発入門」を参照してください。)

2. CORS (Cross-Origin Resource Sharing)

その他の回避方法として、さらにブラウザー依存性が高くなりますが、同じく HTML 5 の仕様を使った Cross-Origin Resource Sharing (CORS) があります。
これは、ブラウザーとサーバー間で、約束された Request / Response を使って、そのドメインからのクロス ドメインの呼び出しを許可するか確認をおこない、もしサーバーが許可していれば XMLHttpRequest を使用したクロス ドメインの呼び出しをおこないます。ブラウザーとサーバーで、下記のような Request / Response のやりとりをします。
(Internet Explorer 8 の場合には、独自の XDomainRequest (xdr) オブジェクトを使用する必要があるなど、いくつかブラウザーに依存した厳しい条件があるので注意してください。「開発者にとっての Internet Explorer」を参照してください。)

HTTP Request

OPTIONS https://contoso.com/someapi HTTP/1.1
Accept: */*
Origin: https://mysite.com
Access-Control-Request-Method: GET
Access-Control-Request-Headers: authorization

HTTP Response

HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Max-Age: 86400
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH
Access-Control-Allow-Headers: authorization
Content-Length: 0

この方法の最大のメリットは、他と方法と比べ もっとも自然な方法である という点です。(他の 2 つの方法は、本来、Cross Domain 接続をするために用意された技術ではありません。) このため、将来、ブラウザーのバージョンがあがっていくと、この手法は本命になってくるでしょう

なお、最新の ASP.NET Web API では、この CORS に対応した Web API の構築もサポートされる予定です。(2013/04 追記 : 「CodePlex : CORS Support for ASP.NET Web API」を参照してください。)

3. JSONP

では、3 番目の JSONP を使用した回避方法について解説します。(以前、こちら にも記載した通り、ややトリッキー (tricky) なことをします。)
上述の方法 (1, 2) では、どれも、比較的最近の仕様を使っていました。一方、ここで説明する JSONP を使った回避方法の最大の利点は、Old Browser でも使用できる という点です。このため、広く一般で使用されるようなサービス (例えば、Bing Map、Google Map など) で多く採用されています。ただし、この方法のデメリットもありますので注意してください。(後述しますが、JSONP は、厳格なセキュリティを要求しないような汎用のサービスに向いています。)
では、簡単にその方法をご紹介します。

まず、HTML におけるスクリプト (script) のソース (src) は、異なるドメイン上に配置されていても参照できるということは、皆さんご存じの通りです。

<script type="text/javascript" src="{.js file of other domain}"></script>

この仕組みを使って、<script> タグを JavaScript などで動的に追加し、参照する .js ファイルの中で答えを返すようにコードを記述します。例えば、以下のような感じです。

var scriptElement = document.createElement('script');
scriptElement.setAttribute('type', 'text/javascript');
scriptElement.setAttribute('src', 'http://contoso.com/service1?callback=func1');
document.body.appendChild(scriptElement);

ここで、http://contoso.com/service1?callback=func1 という記述に注目してください。このように指定することで、結果として返される .js ファイルの中の処理として、下記の通り、func1 の関数を呼び出し、引数に (JSON フォーマットなどの) 結果を渡すように、サービスを構築しておきます。

func1({ 'prop1' : 'result1', 'prop2' : 'result2' });

つまり、ブラウザー上の JavaScript のコードでは、この func1 が呼ばれることを仮定して、以下のような func1 の処理をあらかじめ用意しておくことで、動的にクロス ドメイン接続によってサービスを呼出し、結果をブラウザー上で処理することができます。この手法を JSONP (Json with Padding) と呼びます。

function func1(result)
{
  val1 = result.prop1;
  val2 = result.prop2;
  . . .
}

補足 : なお、jQuery を使用すると、こうした JSONP サービスの呼び出しを 他の Ajax によるサービス呼び出し (XMLHttpRequest) と同じように (シンプルに) 記述できます。

以前、こちら にも記載しましたが、これは、いわゆるセキュリティ ホール (セキュリティ上の弱点) を使った実装ではありません。上記からお分かりの通り、サーバー側 (サービス側) で、このような利用が可能となるように構築しておく必要があります。つまり、ブラウザー側 (クライアント側) の勝手な判断だけで このような接続をおこなうことはできません。

また、この方式からわかるように、JSONP では認証 (Authentication) との組み合わせが困難です。さらに、JSONP サービスに情報 (入力データなど) を渡す場合、GET の URI の一部 (query string など) の形で付帯情報を渡す必要がありますが、こうしたデータも、SSL (Https) を使用した暗号化の対象とはなりません。
つまり、以前も記載しましたが、JSONP のサービスは、厳格なセキュリティが不要な一般的なサービス (広く公開可能なサービス) で使用するようにしてください。(例えば、データの更新をおこなうサービスなどには向いていません。セキュアにするために、いろいろと手の込んだ独自の仕掛けが必要です。)

 

JSONP サービスの構築方法

開発者の方にとっては、「このようなサービスを作るのは面倒」と思うかもしれませんが、JSONP 開発をサポートするいくつかのライブラリーが提供されています。今回は .NET (C# 等) を例に紹介します。

.NET (ASP.NET Web API) をお使いの方は、セキュリティ上の理由から既定では jsonp には対応していませんが、NuGet の WebApiContrib.Formatting.Jsonp パッケージ でこうしたサービスの作成ができます。(「REST サービス (Web Api) で Custom MIME タイプを処理する」で解説している MediaTypeFormatter が使用されています。)
NuGet でパッケージを取得したら、Global.asax.cs に下記の通り設定するだけです。(呼び出す際は、http://. . .?callback=func のようにします。)

. . .
using WebApiContrib.Formatting.Jsonp;
. . .
protected void Application_Start()
{
. . .
  GlobalConfiguration.Configuration.AddJsonpFormatter();
}
. . .

補足 : WCF (.NET 4 以降) をお使いの方は、REST サービスを作成したら、構成ファイル (.config) に、下記 (太字) の通り crossDomainScriptAccessEnabled 属性を 1 行設定します。

補足 : WCF Data Services (旧 ADO.NET Data Services) の場合、プロジェクトにカスタムの Behavior を追加し、サービス クラスのクラス属性として、この Behavior を追加します。
なお、WCF Data Services 5.1.0 以降を使用すると、標準で Json と Jsonp がサポートされてます。

 

SSL で使用する場合の注意

こうしたクロス ドメイン接続をおこなう際に、重要な注意点があります。

例えば、Office 365 (SharePoint Online など) では既定で https (SSL) が使用されますが、Internet Explorer では、https (SSL) のサイトから、http のサイトを呼び出すことは、セキュリティー上の理由から許可されていません。このため、上記の JSONP などを使ってクロス ドメイン接続のための回避をおこなっても、エラーまたは警告が表示されます。

こうしたケースに対応するためには、コールされるサービス側を https (SSL) を使ってホストしておく必要があります。

補足 : 例えば、Bing Maps などでは、https (SSL) を使用して呼び出せるようになっています。クエリー文字列を使って、http を使うか、https を使うか指定できます。

補足 : WCF を使ってサービスを提供する場合は、あらかじめ、サービス側で https (SSL) の要求を受け取れるように、構成ファイル (.config) で Transport の security mode を設定します。
なお、もし、http と https (SSL) の双方で接続可能にするには、エンドポイントを 2 つ作成しておいてください。

 

Plug-in (FLASH, Silverlight など) の場合の対処方法 (ご参考)

さいごに、参考のため、ブラウザー上の FLASH や Silverlight といった Plug-in からクロス ドメイン接続をおこなうケースについて参考のため記載します。今回は、Silverlight を例に記載します。(FLASH も同じ概念です。)

Silverlight のアプリケーションでは、接続先のサーバー (サービス) に clientaccesspolicy.xml、または crossdomain.xml という名前の「ポリシー ファイル」を配置することで、クロス ドメインの接続が許可されるようになっています。(なお、crossdomain.xml のほうは、Flash でも参照されます。) 以下では、すべて、clientaccesspolicy.xml のほうを例に記載します。

まず、配置場所ですが、必ず、そのドメインの Root に配置します。例えば、サービスが http://contoso.com/ServiceDir/Service であっても、http://contoso.com/ServiceDir/clientaccesspolicy.xml ではなく、http://contoso.com/clientaccesspolicy.xml に配置してください。

clientaccesspolicy.xml の内容は、下記の通り記述します。ここでは、http と、tcp のポート 4502 から 4530 までのクロス ドメイン接続を許可しています。

<?xml version="1.0" encoding="utf-8" ?>
<access-policy>
  <cross-domain-access>
    <policy>
      <allow-from http-request-headers="*">
        <domain uri="*" />
      </allow-from>
      <grant-to>
        <resource path="/" include-subpaths="true" />
        <socket-resource port="4502-4530" protocol="tcp" />
      </grant-to>
    </policy>
  </cross-domain-access>
</access-policy>

なお、tcp などのサービス (http 以外のサービス) をホストする場合であっても、http (ポート番号 80) のドメイン ルートのポリシー ファイル (clientaccesspolicy.xml) を見に行くので、必ず、この場所 (ドメイン ルート) に配置しておいてください。(例えば、ある tcp のサービスへのクロス ドメイン接続を開発マシン上でデバッグする場合は、http://localhost/clientaccesspolicy.xml を配置します。)

 

Categories: Uncategorized

Tagged as: ,

23 replies »

Leave a Reply