概要
本連載では、ウィジェット関連の技術動向についてご紹介していきます。第2回となる本稿ではOpenAjax Alliance で仕様が策定されたOpenAjax Hub 2.0の機能と使い方についてご紹介します。特にOpenAjax Hub 2.0では複数のウィジェットを組み合わせたコンポジット・アプリケーションを安全に実現するためのセキュア・マッシュアップ機能が大きな特徴です。マッシュアップによるセキュリティ的な問題と、OpenAjax Hub 2.0の実現するセキュリティの仕組みについて解説します。
なお、一般にアプリケーション開発の世界では、こうした単体である程度の機能を提供するミニアプリという意味だけではなく、GUI(グラフィカルユーザー・インターフェース)を構成するボタンや入力フィールドといった粒度の小さな部品という意味でも“ウィジェット”という言葉が使われます。本稿ではことわりがない限り、“ウィジェット”といえば前者を総称する言葉として使うこととします。
OpenAjax Hub 2.0
OpenAjax Allianceとは、Ajax技術の推進と標準化を行っている業界団体で、主要なベンダやオープンソースプロジェクトなどが参加しています。OpenAjax Allianceでは標準化された仕様を公開するとともに、そのリファレンス実装をオープンソースとして公開しています。
OpenAjax Hub は、ひとつのWebページの中で複数のウィジェットが共存し、お互いに通信を行うためのJavaScript上のフレームワークです。OpenAjax Hubの中核をなすのは、ウィジェット間でメッセージ通信を行うためのPublish/Subscribe API です。
図1はウィジェットを組み合わせた典型的なマッシュアップの一例です。画面の左側にあるウィジェットはデータベースから取得した顧客リストをテーブル上に表示するウィジェットです。顧客リストには個々の顧客の名前や住所、電話番号などが表示されています。画面の右側は地図表示を行うウィジェットです。異なる機能を持つウィジェットを連携させることで、たとえば顧客リストのそれぞれの行をクリックするたびに、その顧客の住所を地図上で表示させるというように、個別に使うよりも便利なアプリケーションを構築することができます。
ここで、ウィジェットを組み合わせている側のアプリケーション(ウィジェットの外側)を、ホストアプリケーションと呼ぶことにします。
OpenAjax Hub仕様では、ホストアプリケーションが「通信ハブ」という機能を提供し、そのハブを通じてホストアプリケーションとウィジェット間、またウィジェット同士の間でメッセージ通信を行うための仕組みを提供します。各ウィジェットはトピック名で識別される名前つきのチャネルを使用し、publish / subscribe (発行・購読)という非同期のメッセージ通信方式を利用します。
メッセージを受信したいとき、ウィジェットはJavaScriptのコールバック関数を登録することにより,特定のトピックを購読します。また、メッセージを送信したいときは、トピック名を指定してハブに対してメッセージを発行します。ハブを介した通信は一対一である必要はなく、一対多、多対多の通信でもかまいません。不特定数のウィジェット間で、ハブを介した通信機能を提供するのがOpenAjax Hub の publish/subscribe APIです。
OpenAjax Hub仕様では、異なるセキュリティ機能を持つ2種類のハブ機能が定義されています。
- 管理されないハブ (Unmanaged Hub) は、ブラウザ上の同一のフレーム内のウィジェット間でメッセージ通信を行うための単純な機能で、セキュリティの仕組みは持ちません。
- 管理されたハブ (Managed Hub) は OpenAjax Hub 2.0で新しく導入された機能で、悪意を持ったウィジェットによる攻撃を防ぐセキュリティ機能を実現します。
マッシュアップによるセキュリティ上の問題とは
ところで、複数のウィジェットを組み合わせてマッシュアップを行った場合に、どんなセキュリティ上の問題が発生するのでしょうか。
セキュリティ機能を使わずにウィジェットのマッシュアップを行った場合、異なるサービス提供者から提供されたウィジェットが同じWebページ上で統合されるため、悪意を持ったウィジェットが紛れ込んでいた場合に、その中に含まれるJavaScriptによって他のウィジェットやホストアプリケーションを攻撃することが可能になります。
例えば前述のマッシュアップされたウィジェットの例で、地図表示ウィジェットの中にもしも悪意のあるスクリプトが含まれていると、
- 顧客リストウィジェットから顧客名や住所、電話番号などを盗む
- 顧客リストに表示されている情報を改ざんする。たとえば住所や電話番号を偽のものに書き換えてユーザを混乱させる。
- 顧客リストウィジェットがバックエンドのサーバにアクセスするための認証されたセッション情報を盗み、セッション乗っ取る
といった攻撃が可能になります。
現在のブラウザのアーキテクチャは、同一起源ポリシー (Same-Origin Policy) と呼ばれるセキュリティモデルを取り入れており、異なるWebサーバからダウンロードされたコンテンツがそれぞれブラウザのウィンドウやフレーム内に表示されたとき、ウィンドウやフレームがサンドボックスの役割を果たして、サンドボックスをまたがったコンテンツ間のアクセスを制限します。しかしひとつのフレーム内にウィジェットがマッシュアップされると、すべてのウィジェットが同じサイトからダウンロードされたとみなされ、ひとつのサンドボックスの中で実行されます。個々のブラウザウィンドウやフレーム上で表示されているコンテンツはブラウザの内部で Document Object Model (DOM)というツリー型のデータ構造で表現され、ブラウザの提供するAPIによってJavaScriptから自由にアクセスすることができます。そのためJavaScriptによって、マッシュアップされた他のウィジェットに属するデータを読み取ったり上書きしたりすることができてしまいます。(図 2(a))
OpenAjax Hub 2.0の管理されたハブ機能を使うと、個々のウィジェットを別のIFRAM(インラインフレーム)内に表示します(図2(b))。このときにウィジェットが別のサーバからダウンロードされるようにすることで、ブラウザの持つサンドボックス機能により、ウィジェット間、ウィジェットとホストアプリケーション間の実行環境が分離され、お互いのDOMやJavaScriptオブジェクトにアクセスできなくなります。ブラウザの同一起源ポリシーは、コンテンツの起源を判定するのにURL内のサーバ名を文字列として比較します。そのため、実体が同じサーバであっても、仮想ホストなどの仕組みにより異なるサーバ名で参照している限り、サンドボックスが機能します。
管理されたハブ(ManagedHub)のセキュリティ機能
管理されたハブによるセキュリティ機能の概要は以下のようなものです。
- 個々のウィジェットをサンドボックス内で実行することにより、DOMやJavaScriptのオブジェクトなどのデータがウィジェットごとに分離されます。つまり、ウィジェット内に悪意を持ったスクリプトが含まれていたとしても、親となるホストアプリケーションや他のウィジェットの持つDOMや JavaScript上のデータやCookieなどにはアクセスできないため、 攻撃を防ぐことができます。
- 個々のウィジェットは、OpenAjax Hubの提供する Publish/Subscribe APIを通じてのみ外部と通信を行うことができます。
- ホストアプリケーションのセキュリティ・マネージャにより、どのウィジェットがどのウィジェットに対してpublish/subscribe通信できるかというセキュリティポリシーを定義することができます。
また、仕様上では実現方法について詳しく定めていませんが、OpenAjax Hub 2.0 の実装は安全性のために次のような機能を実現することが求められています。公開されているOpenAjax Hub 2.0のリファレンス実装ではこれらの機能が実装されています。
- 正しいウィジェットがロードされることを保証するための仕組み
- ウィジェット間での安全な秘匿通信を実現するための仕組み
- メッセージの改ざんや成りすましを防ぐための仕組み
管理されないハブによるpublish/subscribeの例
前述のように、信頼度の異なるウィジェットを単純にマッシュアップすることによるセキュリティ的な脅威が発生するため,管理されたハブのセキュリティ機能が重要になってきます。ただし信頼できるウィジェットを組み合わせてマッシュアップを行う場合(たとえば自分の作成したウィジェットを組み合わせる場合)には、管理されないハブを使用することができます。管理されないハブはプログラムの大きさやパフォーマンスの点で、管理されたハブよりメリットがあります。
管理されないハブに対してメッセージを発行 (publish) するコードの例を以下に示します。
var myData = { x: 12.3, y: 45.6 }; OpenAjax.hub.publish("org.example.myTopic", myData);
ここで“org.example.myTopic”はメッセージを発行するトピックの名前、myData は任意のJavaScriptオブジェクトです。
メッセージを受信する場合は、購読のためのコールバック関数を定義し、トピック名を指定して購読(subscribe) します。
function myCallbackOnDataReceive(topic, data){ alert(“Received data! x = ” + data.x +”, y = ” + data.y); } var subscription = OpenAjax.hub.subscribe(“org.example.myTopic” , myCallbackOnDataReceive);
subscribe関数は、復帰値として subscription というオブジェクトを返します。購読を中止するときにはこの subscription オブジェクトを指定して、unsubscribe 関数を呼び出します。
OpenAjax.hub.unsubscribe(subscription);
管理されたハブによる publish/subscribe の例
管理されたハブ (ManagedHub) では、個々のウィジェットをサンドボックス内で実行することにより、ウィジェットを安全にマッシュアップすることができます。管理されたハブはセキュリティを実現するために、より複雑な構造を持っています。(図3)
管理されたハブを使ってウィジェットのマッシュアップを行う手順のおおまかな流れは以下のとおりです。
- まず、ホストアプリケーションはOpenAjax Hubのライブラリをロードし、管理されたハブのインスタンス ManagedHub を生成し、保持します。
- また,ホストアプリケーションは個々のウィジェットをコンテナの中にロードします。コンテナとはウィジェットのコードをカプセル化するための仕組みの総称ですが、管理されたハブの場合には、サンドボックスを実現するために、HTMLのIFRAME要素がコンテナとして使用されます。
- それぞれのコンテナでは、ハブの機能を使用するためのHubClient の機能が提供されます。ウィジェット内のコードはHubClientを使用し、ManagedHub経由でホストアプリケーションや他のウィジェットと通信を行います。
- また、ホストアプリケーションはセキュリティ・マネージャ機能をコールバック関数として提供する必要があります。ウィジェットがメッセージの publish / subscribe を行った場合に、このコールバック関数が呼び出され、この中でセキュリティポリシーに応じて、処理を中断するか継続するかといった制御を行うことが可能です。
以下に、管理されたハブによるウィジェットのマッシュアップ例を見てみましょう。
準備
管理されたハブではウィジェットをサンドボックス実行するために、各ウィジェットを(少なくとも見た目上)異なるサーバから IFRAMEの中にロードします。一台のマシン上にホストアプリケーションとウィジェットを両方配置するためには、あらかじめ仮想ホストなどの仕組みを使って、同じサーバに異なるサーバ名でアクセスできるようにします.
以下の例では、ホストアプリケーションを mashup.example.com というサーバから、2つのウィジェットをそれぞれ widget1.example.com、widget2.example.com というサーバからダウンロードしています。自分のパソコン上で簡単に試す場合には、たとえば hosts ファイルに
127.0.0.1 mashup.example.com 127.0.0.1 widget1.example.com 127.0.0.1 widget2.example.com
のように記述しておくといいでしょう。
また、OpenAjax Hub 2.0のライブラリをWebサーバ上に展開して、アクセスできるようにしておく必要があります。
以下の例では、http://mashup.example.com/hub20 というURLの下に、公開されているOpenAjax Hub 2.0のリファレンス実装が展開されていることを前提としています。
ホストアプリケーションのソースコード例
以下は、管理されたハブを使って2つウィジェットをマッシュアップするホストアプリケーションの例です。
<html> <head> <title>Sample Host Application using ManagedHub</title> <!-- まず最初に、OpenAjax HubのJavaScriptライブラリをロードする --> <script src="http://mashup.example.com/hub20/release/all/OpenAjaxManagedHub-all.js"> </script> <!-- 必要に応じて他のライブラリのコードをロードする --> <script type="text/javascript"> /* セキュリティ・マネージャのコールバック関数を定義する */ function onMHPublish(topic, data, publisher, subscriber) {return true;} function onMHSubscribe(topic, container) { return true; } function onMHUnsubscribe(topic, container) { return true; } function onMHSecurityAlert(source, alertType) { alert("host app received security alert: source=" + source + ", alertType=" + alertType); } /* ホストアプリケーションがロードされた時に初期化を行う */ function initialize() { /* 管理されたハブのインスタンスを生成する。 引数として、セキュリティ・マネージャ機能のコールバック関数郡を指定する */ var managedHub = new OpenAjax.hub.ManagedHub( { onPublish: onMHPublish, onSubscribe: onMHSubscribe, onUnsubscribe: onMHUnsubscribe, onSecurityAlert: onMHSecurityAlert } ); /* Containerの属性となる、 ウィジェットのセキュリティ警告や接続イベント -のためのコールバック関数を定義 */ var myContainer = { onSecurityAlert : function onClientSecurityAlert(source, alertType) { }, onConnect : function onClientConnect(container) { }, onDisconnect : function onClientDisconnect(container) { } }; var widgetsArea = document.getElementById("mashupWidgets"); /* ウィジェットその1をロード */ var div1 = document.createElement("div"); widgetsArea.appendChild(div1); var container1 = new OpenAjax.hub.IframeContainer(managedHub , "widget1", { Container: myContainer, IframeContainer: { parent: div1, uri: "http://widget1.example.com/samples/widget1.html", tunnelURI: "http://mashup.example.com/hub20/release/all/tunnel.html" } } ); /* ウィジェットその2をロード */ var div2 = document.createElement("div"); widgetsArea.appendChild(div2); var container2 = new OpenAjax.hub.IframeContainer(managedHub , "widget2", { Container: myContainer, IframeContainer: { parent: div2, uri: "http://widget2.example.com/samples/widget2.html", tunnelURI: "http://mashup.example.com/hub20/release/all/tunnel.html" } } ); } </script> <body onload="initialize()"> This is a sample host application. <div id="mashupWidgets"></div> </body> </html>
この例では、<div id=” mashupWidgets”> というDIV要素の下に2つのDIV要素を子要素として挿入し、それぞれの中にiframeコンテナを使ってウィジェットをロードしています。
OpenAjax.hub.IframeContainer のコンストラクタに対して3つの引数を指定しています。
- 第1引数:ウィジェットの通信を管理するManagedHub のインスタンス
- 第2引数:ウィジェットの名前
- 第3引数:ContainerとIframeContainerという二つのプロパティを持つオブジェクト
この第3引数のうち、Containerプロパティには、コールバック関数群を含むオブジェクトを指定します。IframeContainerプロパティには、次の3つのプロパティを持つオブジェクトを指定します。
- parent :ウィジェットを挿入するHTML親要素への参照
- uri:ウィジェットのコンテンツとなるHTMLファイルのURL。このURLは、「準備」の項で説明したように、ホストアプリケーションと異なるサーバ名を参照する必要があります。この例では ウィジェット1がwidget1.example.com、ウィジェット2が widget2.example.comというURLを指定しています。
- tunnelURI :管理されたハブでウィジェットが通信を行う際に内部的に使用するトンネル機能のURLを指定します。トンネル機能はOpenAjax Hub 2.0の実装の一部として、JavaScriptを含むHTMLファイルなどの形態で提供されます。この例では release/all/tunnel.html という OpenAjax Hub 2.0のリファレンス実装に含まれているファイルを参照しています。
このサンプル・アプリケーションをブラウザ上で表示したイメージを図4に示します。
ウィジェットのソースコード例
以下は、上記のホストアプリケーションに組み込まれるウィジェットの例です。
<html> <head> <title>Widget 1</title> <!-- まず最初に、OpenAjax HubのJavaScriptライブラリをロードする --> <script src="http://widget1.example.com/hub20/release/all/OpenAjaxManagedHub-all.js"> </script> <script type="text/javascript"> /* セキュリティ警告発生時のコールバック関数 */ function clientSecurityAlertHandler(source, alertType) { alert("received a security alert: source=" + source + ", alertType=" + alertType); } /* メッセージ受信用のコールバック関数 */ function myCallbackFunc(topic, data){ /* 受信したメッセージを表示する */ var msg = document.getElementById("receivedMessage").value = data; } /* ManagedHubに接続した際に呼ばれるコールバック関数 */ function connectCompleted( hubClient, success, error ) { if (success) { /* ここで任意の処理を行う. 下はトピックを購読する例 */ hubClient.subscribe("com.example.test.topicB", myCallbackFunc); } } /* ウィジェットがロードされたときに呼び出されるイベントハンドラ */ function initialize() { /* HubClient のインスタンスを初期化する */ hubClient = new OpenAjax.hub.IframeHubClient({ HubClient: { onSecurityAlert: clientSecurityAlertHandler } }); /* ホストアプリケーションのManagedHubに接続する */ hubClient.connect( connectCompleted ); } /* ボタンが押された場合のイベントハンドラ */ function sendMsg() { var msg = document.getElementById("messageToSend").value; hubClient.publish("com.example.test.topicA", msg); } </script> </head> <body onload="initialize()"> <div style="border:black"> <h3>Widget1</h3> <p> Received from Topic B: <input type="text" id="receivedMessage"> <br /> Sending to Topic A: <input type="text" id="messageToSend" value="hello from widget1" > <input type="button" value="Send" onclick="sendMsg()"/><br /> </p> </div> </body> </html>
この例では、まずセキュリティ警告発生時、メッセージ受信時、ManagedHubへの接続完了時にそれぞれ呼ばれるコールバック関数を定義しています。
次に、ウィジェットがロードされたときに呼び出される initialize 関数の中で、 OpenAjax.hub.IframeHubClient のコンストラクタによって HubClientのインスタンスを初期化しています。このHubClientインスタンスの connect関数を使用してManagedHubへの接続を行います。接続完了時に、あらかじめ定義した connectCompleted というコールバック関数が呼びだされます。
connectCompleted関数の中では任意の処理を行えばよいのですが、この例では “com.example.test.topicB” というトピックへの購読を行い、メッセージを受信したときにその内容を画面に表示する処理を行う myCallbackFunc 関数をコールバック関数として登録しています。
一方で、ウィジェットの内部(body要素)にはメッセージ表示用と送信用のテキストフィールドが定義されており、信用のテキストフィールドにメッセージを入力してボタンを押すと、メッセージが “com.example.test.topicA” というチャネルに対して発行されます。つまり、この例のWidget 1は、com.example.test.topicAをメッセージ送信用、com.example.test.topicBをメッセージ受信用のチャネルとして使用しています。もうひとつのウィジェットでは逆にcom.example.test.topicB をメッセージ送信用、com.example.test.topicA をメッセージ受信用のチャネルとして利用すれば、二つのウィジェットの間で双方向にメッセージをやりとりすることが可能です。
OpenAjax Metadata仕様におけるマッシュアップ可能なウィジェット(Mashable Widget)
OpenAjax Metadataとは、Ajaxツールキットの互換性を確保するためにAjaxのウィジェットやAPIのためのメタデータをXMLで表現する形式を定義した仕様で、現在バージョン1.0のドラフト版が公開されています。
OpenAjax Metadata仕様の対象とするウィジェットは、コンボボックスやメニューバーのようないわゆるUIコンポーネントと、前節で述べたような独立した機能を提供するウィジェットの両方を含みます。この仕様中では、特に他のウィジェットと連携してマッシュアップを可能とするようなウィジェットを mashable widgetと呼んでいます。ウィジェットを他のウィジェットと連携させ、マッシュアップ可能にするためには次のようなメタデータを定義する必要があります。
- <widget> 要素の jsClass属性: <widget> はウィジェットのメタデータ定義のルート要素です。<widget> 要素のjsClass属性にはウィジェットの振る舞いを実装したJavaScriptクラス名を指定します。この属性を指定することで、そのウィジェットが mashable widgetであることを宣言することになります。同時に、そのウィジェットはOpenAjax Metadata仕様に定義されたWidget APIをサポートする必要があります。
- <property> 要素の sharedAs 属性 :デフォルトではウィジェットのプロパティは他のウィジェットから参照することができませんが、sharedAs属性を指定することでパブリック・プロパティとしてウィジェット間で参照することができるようになります。
- <topic> 要素: ウィジェットが発行 (publish) したり購読 (subscribe) したりするメッセージ・トピックを指定します。ウィジェットは、OpenAjax Hub 2.0 HubClient クラスのpublish関数とsubscribe関数を使ってメッセージを発行したり購読したりできます。
また、 mashable widgetを実装した JavaScript クラスの中で、以下のようなWidget APIを実装する必要があります。
- ウィジェットのロードやアンロードなどのライフサイクルイベント処理
- ウィジェットのプロパティの読み書きや、プロパティ変更時のイベント処理
- topicへのメッセージの発行・購読などのイベント処理
- ウィジェットのサイズ制御
- モード(view, edit, help)
- ドメインをまたがったHTTPリクエストをプロキシ経由でアクセスためのURL書き換え処理
その他に、マッシュアップを円滑に行うために、以下のような属性を使用可能です。
- <widget> 要素の sandbox 属性により、ウィジェットを iframeによるサンドボックスに閉じ込めるべきかどうかのヒントを指定する
- <widget> 要素の scrolling 属性により、ウィジェットにスクロールバーを表示するかどうかを指定する
- <widget> 要素の singleton 属性により、そのウィジェットがWebページ内に一つ以上存在してもよいかどうかを指定する
- <content> 要素の mode 属性により、そのウィジェットが閲覧 (view) モードか編集 (edit) モードか、などを指定する
まとめ
今回は、ブラウザ上でマッシュアップ可能なウィジェットを扱うための標準仕様であるOpenAjax Hub 2.0仕様 と OpenAjax Metadata 1.0仕様についてサンプルコードを交えてご紹介しました。
今回ご紹介したコードについては、OpenAjax Allianceにより公開されているOpenAjax Hub 2.0 のリファレンス実装により動作確認を行いました。OpenAjax Hub やOpenAjax Metadataの詳細については、 OpenAjax Allianceにより公開されている仕様をご覧ください。
参考文献
- OpenAjax Hub 2.0の詳細な仕様については、OpenAjax Hub 2.0 Specificationをご覧ください。
- OpenAjax Metadata のドラフト仕様は、こちらで公開されています。