目次


クライアント・サイドのソリューションによってクロスドメインの通信を改善する

SOP の制約を回避する

Comments

はじめに

複数の Web サイトが協調して動作する必要のある場合が増えています。例えばオンラインで賃貸物件を紹介する Web サイトの場合、特定の賃貸物件の場所を示すために Google マップをサポートする必要があります。そうしたニーズに対応するため、さまざまな種類のマッシュアップが登場しています。マッシュアップは、さまざまなプロバイダーのデータやコンポーネントを統合し、より実用的な Web アプリケーションへとカスタマイズしたものです。マッシュアップ、つまりコラボレーション機能は、Web 2.0 の重要な部分と考えられています。

驚いたことに、Ajax (Asynchronous JavaScript and XML) とマッシュアップとの組み合わせは容易ではありません。ブラウザーによって課せられるセキュリティー制約により、ページ上のさまざまなウィジェット同士を互いに通信させることも簡単ではありません。この問題を解決する方法として以前から、サーバー・サイドにプロキシーを構築する方法がありますが、この方法はスケーラブルではありません。この記事では、これとは別のソリューションとして、クライアント・サイドでクロスドメインの通信やデータ転送を行う方法をいくつか学びます。

セキュリティー制約

SOP (Same Origin Policy: 同一生成元ポリシー) は、1 つの生成元からロードしたスクリプトによって別の生成元の文書内のプロパティーやメソッドが取得または操作されるのを防止するためのものです。origin (生成元) という用語は、ドメイン名、アプリケーションのプロトコル、スクリプトを実行する文書のポートの組み合わせを表します。しかし、SOP の概念については誤解されている場合があります。SOP は、単にサイト A がサイト B から情報を取得することができない、というだけではありません。SOP 制約の下で何ができ、何ができないかを理解する必要があります。

SOP による制約

例えば、生成元 A の Web ページでは以下の内容を実行することができます。

  • 生成元 B のスクリプト、CSS スタイルシート、または画像を取得する
  • 生成元 B のページを指す iframe/frame を含める
  • HTML 要素 (iframeimg) の src 属性を使用して生成元 B に何らかの情報を送信する

生成元 A の Web ページでは、以下の内容を実行することはできません。

  • 生成元 B に対して Ajax 呼び出しを行う
  • ページ B を指す iframe/frame のコンテンツを読み取ったり、操作したりする

なぜこのような制約があるかというと、それは主にユーザーの重要な情報を保護するためです。SOP の前提となっているのは、ユーザーがプロバイダーとやり取りする場合、そのユーザーはそのサイトに送信された情報が他のサイトに漏れてしまうことを望まない、という考え方なのです。この種の制約により、異なる Web サイト間で行う協調型作業は制限を受けますが、ユーザーは潜在する有害な攻撃から保護されます。

この問題に対処するためのソリューションは数多くあります。例えば、JSONP は Web ページが任意のソースから動的にスクリプトをロードできるという事実を利用しています。ただし JSONP には主な制約として、Ajax 呼び出しのようなエラー処理メカニズムがないこと、そして script タグを要求するリクエストのメソッドは GET であるため、長さに制約があること、の 2 つが挙げられます (JSONP については「参考文献」セクションを参照)。

以下のセクションでは、クロスドメインの通信やデータ転送を行うための、クライアント・サイドのソリューションについて説明します。各ソリューションには長所と短所があり、どの方法を選択するかはアプリケーションのシナリオに大きく依存します。

クロスサブドメインによるソリューション

生成元 A と生成元 B のスーパー・ドメインが共通の場合、document.domain プロパティーを変更すると 2 つの文書同士が容易に互いの文書にアクセスできるようになります。document.domain は HTML 仕様に規定された読み取り専用のプロパティーです。最近のほとんどのブラウザーでは、document.domain をスーパー・ドメイン (最上位レベルではありません) に設定できるようになっています。例えば URL が www.myapp.com/index.html の文書はその文書自身のドメインを myapp.com と設定する一方、sample.myapp.com/index2.html にある別の文書もその文書自身のドメインを myapp.com と設定することができます。図 1 は document.domain がどのように機能するかを示しています。

図 1. document.domain
スーパー・ドメインが同じで URL が異なる文書同士が通信する方法として、各文書のドメインを同じスーパー・ドメインとして設定する方法が説明されています。
スーパー・ドメインが同じで URL が異なる文書同士が通信する方法として、各文書のドメインを同じスーパー・ドメインとして設定する方法が説明されています。

このクロスサブドメインによるソリューションを使用すると、サブドメインが異なる生成元同士が同じスーパー・ドメインの下で通信することができ、SOP の制約を受けません。しかし厳密に言えば、クロスサブドメインによるソリューションが最も適しているのはイントラネット・アプリケーションです。

URLのハッシュ (フラグメント id) によるソリューション

URL は下記の図 2 のようにいくつかの部分で構成されています。

図 2. URL の構成要素
URL のスクリーン・ショットを示しており、URL のさまざまな要素 (プロトコル、ホスト、パス名、クエリー、ハッシュ) を線で区切って規定しています。
URL のスクリーン・ショットを示しており、URL のさまざまな要素 (プロトコル、ホスト、パス名、クエリー、ハッシュ) を線で区切って規定しています。

通常は URL を少しでも変更すると新しい Web ページがロードされます。例外はハッシュ値を変更した場合です。URL のハッシュをどのように変更しても、ページが更新されることはありません。ハッシュは、既に多くの Web 2.0 サイトで部分的にページを更新する際に各ステップにブックマークを付けるために広く使用されています。クロスドメインの通信でも、ハッシュは貴重な資産です。生成元の異なる文書同士が互いのハッシュ値を取得する際には制約がありますが、その文書同士が (ハッシュ値を含めて) 互いの URL を設定することができます。ハッシュを使用すると、生成元の異なる文書同士が互いにメッセージを送信することができます。図 3 はその一例を示しています。

図 3. URL のハッシュ (フラグメント id) を使用した通信
URL が異なる文書同士が通信する方法として、互いに相手のハッシュ値を変更し、自分自身のハッシュの変化をモニタリングする方法が説明されています。
URL が異なる文書同士が通信する方法として、互いに相手のハッシュ値を変更し、自分自身のハッシュの変化をモニタリングする方法が説明されています。

図 3 の場合、A が B にメッセージを送信しようとする際には A が B のハッシュ値を変更します (リスト 1)。

リスト 1. URL のハッシュによってメッセージを送信する
function sendMsg(originURL, msg){
	var data = {from:originURL, msg:msg};
	var src = originURL + “#” + dojo.toJson(data);
	document.getElementById('domainB').src=src;
}

B の関数は B 自身のハッシュ値をポーリングし、A が何を送信したかを検出します。同じ方法で B は A に応答することができます。A がそのメッセージを受信したい場合には、A も A 自身のハッシュ値をポーリングする必要があります。

リスト 2. URL のハッシュをモニタリングし、そのハッシュからメッセージを受信する
window.oldHash="";
checkMessage = function(){
	var newHash = window.location.hash;
	if(newHash.length > 1){
		newHash = newHash.substring(1,newHash.length);
		if(newHash != oldHash){
 		oldHash = newHash;
 		var msgs = dojo.fromJson(newHash);
 		var origin = msgs.from;
 		var msg = msgs.msg;
 			 sendMessage(origin, "Hello document A");
 		 }
 	}
}
window.setInterval(checkMessage, 1000);
sendMessage = function(target, msg){
	var hash = "msg="+ msg;
	parent.location.href= target + “#” + hash;
}

JSONP の場合と同様、この方法にも長さの制約がありますが、この方法のほうが JSONP の場合よりも適切にエラーを処理することができます。一部の特殊文字 (疑問符 (?) など) は URL 用として予約されているため、最初にエンコードする必要があります。

リスト 3. 特殊文字を含むメッセージを URL のハッシュを使用して送信する
function sendMsg(originURL, msg){
	…
	var src = originURL + “#” + encodeURI (dojo.toJson(data));
	…
}

そして受信する場合には最初に特殊文字をデコードする必要があります。

リスト 4. 特殊文字を含むメッセージを受信する
function checkMsg(){
	…
	var msgs = decodeURI(dojo.fromJson(newHash)); 
	…
}

クロスフラグメントによる手法

ハッシュは既に多くの Web サイトで他の目的に使用されているため、それらの Web サイトでクロスドメインの通信やデータ転送のために URL のハッシュ (フラグメント id) による手法を使おうとすると複雑になります。クロスフレームのメッセージングはフラグメント id によるメッセージングといくらか似ています。図 4 はクロスフラグメントによる手法の動作を示しています。

図 4. クロスフラグメントによる手法
URL が異なる文書同士が通信する方法として、ターゲット文書と同じドメインのプロキシーを指す IFrame を動的に作成し、その IFrame がターゲット文書内の該当する関数を呼び出してリクエストとデータを取得し、応答する方法が説明されています。
URL が異なる文書同士が通信する方法として、ターゲット文書と同じドメインのプロキシーを指す IFrame を動的に作成し、その IFrame がターゲット文書内の該当する関数を呼び出してリクエストとデータを取得し、応答する方法が説明されています。

上記の図で A が IFrame B と通信しようとする場合、A はまず自分自身の中に IFrame を作成します。この IFrame は B と同じドメインにあるプロキシー C を指します。C はプロキシーの URL の中に、B のパラメーターまたはデータ、さらには B のフレーム識別子を含んでいます。

リスト 5. クロスフラグメントによる手法でメッセージを送信する
function sendMsg(msg){
 var frame = document.createElement(“iframe”);
 var baseProxy = “http://www.otherapp.com/proxy.html”;
 var request = {frameName:’otherApp’,data:msg};
 frame.src = baseProxy+”#”+encodeURI (dojo.toJson(request));
 frame.style.display=”none”;
 document.body.appendChild(frame);
}

C がロードされると、C は A のリクエストとデータを取得し、対応する B のメソッドを呼び出します。B と C は同じドメインにあるため、相手のウィンドウのハンドラーによって相手のメソッドを直接呼び出すことができます。その結果、A は B に適切にメッセージを送信することができ、また B は同じ方法で応答することができます。

リスト 6. クロスフラグメントによる手法でメッセージを受信する
window.onLoad = function(){
     var hash = window.location.hash;
     if(hash && hash.length>1){
          var request = hash.substring(1,hash.length);
          var obj = dojo.fromJson(decodeURI (request));
          var data = obj.data;
          //process data
          parent.frames[obj.frameName].getData(…);// getData in a function defined in B
     }
}

OpenAjax を実装する

OpenAjax には、フラグメント id とクロスフレームによる手法に基づいてクロスドメインの通信やデータ転送をサポートするマネージド・ハブ・モジュールが用意されています。マネージド・ハブ・モジュールにはマネージャー・サイドとクライアント・サイドが含まれています。マネージド・ハブには、メッセージを格納するためのメッセージ・ハブが含まれています。他と通信しようとする各ウィジェットは、そのウィジェット自身の中にハブ・クライアントを構築し、そのハブ・クライアントに接続するための対応するコンテナーも構築します。コンテナーはクライアントに代わってマネージド・ハブとやり取りします。クライアント・サイドはパブリッシュ/サブスクライブ・メカニズムを使用してメッセージを送受信することができます。OpenAjax の基本的な動作フローを示したものが図 5 です。

図 5. OpenAjax のメイン動作フロー
OpenAjax を使用してクロスドメインの通信を実現するためのメイン・ワークフローが説明されています。
OpenAjax を使用してクロスドメインの通信を実現するためのメイン・ワークフローが説明されています。

window.name によるソリューション

window.name は、クロスドメインの通信やデータ転送を行うための、扱いに注意が必要なソリューションです。通常、window.name は以下のように使用されます。

  • window.frames[windowName] を使用して子ウィンドウを取得する
  • リンク要素の中でターゲット属性として window.name を設定する

window.name には、生成元が異なる文書同士の「橋渡し」に適した、注目に値する性質があるにもかかわらず、このプロパティーは、頻繁には使用されてはいません。どのようなページをロードする場合であれ、window.name の値は同じままです。では SOP 制約の下で window.name をどのように使用することができるのでしょう?図 6 は window.name を使用してクロスドメインの通信を行う方法を示しています。

図 6. window.name とクロスドメインの通信
他のドメインからリソースを取得する方法として、window.name を使用してサーバー・レスポンスを格納し、クライアント・サイドがロールバックして window.name の値をチェックする方法が説明されています。
他のドメインからリソースを取得する方法として、window.name を使用してサーバー・レスポンスを格納し、クライアント・サイドがロールバックして window.name の値をチェックする方法が説明されています。

ページ A が別の生成元のリソースや Web サービスを取得しようとする場合、ページ A はその外部リソースや外部サービスをターゲットとする新しい隠し IFrame B をページ A 内に追加します。サーバーは HTML ファイルで応答するため、データには window.name プロパティーが設定されます。ページ A と IFrame B は同じドメインではないので、相変わらず A は B の name プロパティーを取得することはできません。B がデータを取得した後、B を A と同じドメインの任意のページに戻すと、A から name プロパティーにアクセスできるようになります。A がデータを取得した後は、いつでも B を破棄することができます。

dojox.io.windowName を使用してクロスドメインの通信を行う

Dojo には window.name を使用してクロスドメインの通信を行うための手段が用意されています。そのための唯一の API は dojox.io.windowName.send(method, args) です。この API は dojo.xhrGet/dojo.xhrPost と似ています。メソッドは GET または POST であり、argsdojo.xhrargs と似ています。例えば、リスト 7 のようにしてクライアント・サイドでクロスドメインのリクエストを送信することができます。

リスト 7. window.name によってメッセージを送信する
var args = {
 url: "http://www.sample.com/testServlet?windowName=true",
 load: function(data){
 alert("You've got the data from server " + data);
    },
error: function(error){
 alert("Error occurred: " + error);
 }
}
dojox.io.windowName.send("GET",args);

dojox.io.windowName の使い方は dojo.xhr の使い方と同じです。サーバー・サイドで windowname トランスポートによってリソースやサービスを利用できるようにしたい場合には、リクエストの中にある windowName パラメーターをチェックします。リクエストに windowName パラメーターが含まれている場合、サーバーが応答する HTML 文書の window.name にはクライアントに送信する必要のあるデータが設定されている必要があります。リスト 8 のコードはその一例を示しています。

リスト 8. window.name によるメッセージ送信手法をバックエンドでサポートする
testServlet.java:
protected void doGet(HttpServletRequest request,HttpServletResponse response){
 //process request
 String returnData = ...;
 String isWindowNameReq = request.getParameter(“windowName”);
 if(null !=isWindowNameReq && Boolean.parseBoolean(isWindowNameReq)){
	 returnData = getCrossDomainStr(returnData);
}
 response.getOutputStream().print(returnData);
}
private String getCrossDomainStr(String data){
 StringBuffer returnStr = new StringBuffer();
 returnStr.append("<html><head><script type=\"text/javascript\">window.name='");
 returnStr.append(data);
 returnStr.append("'</script></head><body></body></html>");
 return returnStr.toString();
}

生成元ドメインの任意のページにフレームを戻す際には、そのページがそのドメイン内に存在していることを確認する必要があります。ページが存在しない場合、Internet Explorer で問題が発生します。Firefox では単純に blank.html を使用することができます。Dojo の場合、どのページに戻るかを dojo.dojoBlankHtmlUrl プロパティーによって指定することができます。デフォルトで、そのページは Dojo ライブラリーの下にある dojo/resources/blank.html に設定されます。

window.name を使用して転送されるデータ量は URL のハッシュを使用する場合よりもはるかに大量です。最近のほとんどのブラウザーでは、window.name を使用して 16M を超えるデータを転送することができます。

HTML5 の新機能

HTML5 仕様のドラフトには、クロスドメインの通信を安全に行うための新しいメソッド、window.postMessage(message, targetOrigin) があります。このメソッドが呼び出されると、メッセージ・イベントがディスパッチされ、ウィンドウがそのメッセージ・イベントをリッスンしていると、そのウィンドウはメッセージとメッセージの生成元をイベントから取得することができます。図 7 はその一例を示しています。

図 7. HTML5 でのクロスドメインの通信
HTML5 のネイティブ関数 (window.postMessage) とメッセージ・イベント・リスナーによるクロスドメイン通信が説明されています。

図 7 で IFrame のロードが成功した場合に、IFrame がそのことを別の生成元の親ウィンドウに通知したいときには、IFrame は window.postMessage によってメッセージを送信します。それと同時に、IFrame はフィードバック・メッセージをモニタリングします (リスト 9)。

リスト 9. HTML5 の新しいメソッドによってメッセージを送信する
http://www.otherapp.com/index.html
function postMessage(msg){
     var targetWindow = parent.window;
      targetWindow.postMessage(msg,"*");
}
function handleReceive(msg){
 var object = dojo.fromJson(msg);
 if(object.status == “ok”){
	//continue to do other things
	……
 }else{
	//retry sending msg
	……
 }
}
window.addEventListener("message", handleReceive, false);
window.onLoad = function(){
    postMessage("already loaded");
}

親文書はメッセージ・イベントをリッスンします。メッセージを受信すると、まずそのメッセージが www.otherapp.com から送信されたものかどうかがチェックされてから、受信確認メッセージが返されます。

リスト 10. HTML5 の新しいメソッドによってメッセージを受信する
http://www.myapp.com/index.html
function handleReceive(event){ 
    if(event.origin != "http://www.otherapp.com")
        return; 
     //process data
     ……
     var otherAppFrame = document.getElementById(“otherApp”) 
     otherAppFrame.postMessage(“{status:’ok’}”,”http://www.otherapp.com”);
}
window.addEventListener("message", handleReceive, false);

リスト 10 のサンプル・コードは、Firefox 3 またはそれ以降、Internet Explorer 8、Google Chrome 2、Opera 9 またはそれ以降、Safari 4 で実行することができます。このコードにより、生成元が異なる文書間で通信を行うことが容易にできるようになります。また、皆さん自身の文書に他の文書からのメッセージを受信させたくない場合には、メッセージ・イベント・リスナーを追加してはならず、またすべてのメッセージを削除する必要があります。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Web development
ArticleID=742568
ArticleTitle=クライアント・サイドのソリューションによってクロスドメインの通信を改善する
publish-date=07292011