フロントエンドWeb戦略室

第3回localStorageとpostMessageの使いどころ(2)

postMessage

一般的に広く使われている、URLの?以降の文字列(query string)を使いサーバに対してデータを受け渡す方式は、異なるドメインのJavaScript同士で通信する際にはいくつかのデメリットがあります。http://example.com/?query_stringというURLにアクセスするとquery_stringの部分がサーバに送信されます。当然新規の通信が発生しますし、どのようなメッセージが送信されたのかをJavaScriptから受け取るには、サーバがブラウザに対して応答を返すまで待たなければなりません[3]⁠。postMessageの登場以前も、サーバサイドを経由しない、JavaScriptだけで完結するクロスドメインでのメッセージ送信手法が考えられてきました。代表的なものは、window.name[4]を使った方法リスト1とlocation.hashを使った方法リスト2です。

リスト1 window.nameを使ったメッセージ送信
<iframe src="http://example.com/" name="message">
リスト2 location.hashを使ったメッセージ送信
http://example.com/#location.hash

postMessage登場以前

リスト1のname="message"の部分はJavaScriptの変数window.nameとして読み込まれたiframeから参照することができます。この際にwindow.nameはサーバに送信されることがありません。また、URLの#以降、location.hashやハッシュフラグメントと呼ばれる部分も、自動的に送信されることがありません。query stringを使う方式と違って、一度キャッシュしておけばサーバとの通信が発生せず、オフラインの状態であっても、JavaScriptから送られてきたメッセージを参照することができます。ブラウザにキャッシュ済みのURLを、window.nameやlocation.hashを変化させて読み込むことで、サーバとの通信を発生させずに、相手先ドメインのJavaScriptに変数を受け渡すことを可能にしているのです。送信元ドメインのiframeを読み込んで、同様にlocation.hashやwindow.nameを設定することで、受け取ったメッセージに対する返信を行えます。こういった旧来のクロスドメインメッセージング手法は、複数のiframeをネストさせて読み込む必要がありますし、さまざまなブラウザで動作させるためにコードが複雑になりがちでした。postMessageはこの問題を解決してくれます。

postMessageの誕生

HTML5でpostMessageが導入されたことで、JavaScriptだけで完結するクロスドメインの送信手段が劇的に改善しました。MDNでは、

window.postMessageは、安全にクロスドメイン通信を可能にするためのメソッドです。

と紹介されていますが、実はpostMessageはクロスドメイン通信のみならず、さまざまなシーンで汎用的に使われるメッセージングのしくみと考えることができます。postMessageがサポートされているのは、Webページ内のwindowオブジェクトだけではありません。表示中のHTMLと別スレッドでJavaScriptを実行するWeb Workersとの通信や、ブラウザ拡張機能との通信などさまざまなシーンで「相手を指定して、メッセージを送る」という機能がサポートされています。そして、多くの場合、postMessageと共通のAPI[5]により実現されています。

クロスドメインのためのAPI設計

写真サービスhttp://photo.example.com/ブログサービスhttp://blog.example.jp/があると想定します。それぞれのサービスは別組織が運営していて、ログイン情報は共有していません。ここで、ブログに貼り付ける画像を写真サービスから選択してみます。写真サービスにログイン中のユーザのアルバムから、画像URLをもらってくることにします。

ダメな例:パスワードを預かる

photo.example.comのユーザidとパスワードをblog.example.jpが預かって、代理でアルバムの写真の一覧を取得できるようにします。しかしこれではblog.example.jp側のミスでパスワードが漏洩(ろうえい)するリスクがありますし、悪用されても検知できません。

ダメな例:JSONP

ブラウザから直接photo.example.comのデータを参照します。http://photo.example.com/api/album/listというAPIを用意して、JSONPで写真の一覧を返すようにするとしましょう。

  • ログイン中のCookieを送ることでユーザを識別する
  • JSONPもしくは、CORSCross-Origin Resource Sharingにより許可されたXMLHttpRequestでリソースを取得する

こういったAPIを作ってしまうと、ログイン中のユーザidであったり保存している写真の一覧を誰でも取ることができてしまいます。

APIを用意してJSONPで写真の一覧を返す方法についてもう少し深く、photo.example.comblog.example.jp「全面的に信頼する」という前提で、blog.example.jpからのみ、APIの呼び出しを許可することを考えてみましょう。

JSONPは呼び出し元を制限することが困難です。リファラを参照することで想定と異なるドメインから呼び出されたことを検出できますが、リファラは送信が必須のヘッダではありません。ブラウザの設定やセキュリティソフトによっては送られないこともありますので、特定のドメインから呼び出されたということを確実に判定することができません。クロスドメインのXMLHttpRequestの場合、originヘッダを参照することで、特定のドメインからのみクロスドメイン通信を許可することができます[6]⁠。しかしアクセス元を制限したとしても、やはり問題は残ります。ユーザがサードパーティCookieを無効化していた場合は、XMLHttpRequestの設定よりも、ユーザの設定が優先されることになります。つまり、ログインCookieをそのまま使ったJSONP APIを作ってしまうことは主に2種類の問題があります。1つは本来ユーザの許可が必要なデータを不用意に返してしまうなど実装ミスを引き起こしやすいこと。2つ目はユーザがブラウザの設定を変更していた場合[7]⁠、正常に動作しなくなることです。

良い例:OAuthを使う

初回アクセス時、photo.example.comにリダイレクトしてphoto.example.comblog.example.jpに対し、⁠写真の読み取り権限を与えて良いですか?」とユーザに対して確認をするのは良い方法でしょう。ユーザが許可した場合、アルバムに保存してある写真の一覧にアクセス可能、という権限を与えます。http://photo.example.com/api/album/listに対するJSONPやクロスドメインXMLHttpRequestのリクエストに、アクセストークンを付加することで「どのサービスからの呼び出しであるのか」⁠誰がリクエストしているのか」を判別できるようになります。

postMessageを使ったクロスドメイン通信

ここまで見てきたように、APIを設計するときにはさまざまなことを考えなければなりません。しかしpostMessageを使う場合は、こういったAPIの設計は極端にシンプルになります。blog.example.jpからphoto.example.comの中身はドメインが異なるため、本来であれば直接読み取れません。これはiframeで読み込んだ場合でもポップアップウィンドウで読み込んだ場合でも同じです。ユーザはphoto.example.comドメイン上でブログに貼り付けたい写真を選択し、blog.example.jpドメインに対して張り付けたい画像のURLをpostMessageで送信します。postMessageを使った呼び出し元の実装例図1リスト3に、ポップアップウィンドウで開かれる選択画面の実装例図2リスト4に示します。ブログサービスが写真共有サービスから写真のURLを受け取る機能を想定しており、異なるドメインで動作することを確認します。postMessageを使うことで、異なるドメイン上の変数に間接的にアクセスすることができます。リスト3、リスト4を見るとわかりますが、

  • ① windowを開く
  • ② 呼び出し元を確認しつつ写真を選択する
  • ③ 事前にアクセストークンを交換する

など、煩雑な作業を一切行わずに済みますね。ブログサービス側からすれば「単に画像のURLを受け取れればよい」わけで、

  • 外部でどんなサービスを使っているのか
  • 誰としてログインしているのか
  • 貼り付ける画像以外に、ユーザがどんな画像を持っているのか

本来知る必要がない情報です。

図1 呼び出し元
図1 呼び出し元
リスト3 呼び出し元。iframe内に読み込んで選択する
<html>
<head><meta charset="utf-8">
</head>
<body>
<button onclick="run_selector()">写真を選択</button><br>
state<input id="state" size="100"><br>
result<input id="result" size="100"><br>

<script>
var photo_url = "http://photo.example.com/photo.html";
var photo_origin = "http://photo.example.com";
var timer;

function $(id){ return document.getElementById(id) }

window.onmessage = function(event){
    console.log(event);
    if (event.origin === photo_origin) {
        clearInterval(timer);
        var result = JSON.parse(event.data);
        $("state").value = result.state;
        $("result").value = result.result;
    }
};

function run_selector(){
    popup = window.open(photo_url, "",
"width=300,height=300");
    timer = setInterval(function(){
        popup.postMessage("select_photo", photo_origin);
    }, 100);
}
</script>
</body>
</html>
図2 ポップアップウィンドウ
図2 ポップアップウィンドウ
リスト4 ポップアップウィンドウで開かれる写真選択ページ
<html>
<head><meta charset="utf-8"></head>
<body>
<p><span id="source"></span> が写真を求めています</p>
<input name="photo" type="checkbox" checked value="http://photo.example.com/1.jpg">写真1<br>
<input name="photo" type="checkbox" checked value="http://photo.example.com/2.jpg">写真2<br>
<input name="photo" type="checkbox" checked value="http://photo.example.com/3.jpg">写真3<br>
<input name="photo" type="checkbox" checked value="http://photo.example.com/4.jpg">写真4<br>

<button onclick="do_response({state:'選択しました', result: selected()});window.close()">許可</button>
<button onclick="do_response({state:'拒否されました'});window.close()">拒否</button>

<script>
var opener_origin = "";
function $(id){ return document.getElementById(id) }
function selected(){
    var result = [];
    var input = document.querySelectorAll("input");
    for (var i=0; i < input.length; i++) {
        if (input[i].checked) result.push(input[i].value);
    }
    return result;
}

window.onmessage = function(event){
    $("source").innerHTML = event.origin;
    opener_origin = event.origin;
    do_response({ state: "準備中", result: "まだ選択されていません"}, opener_origin);
}

function do_response(res){
    window.opener.postMessage(JSON.stringify(res), opener_origin);
}
</script>
</body></html>

OAuthを使って、ブログサービス事業者に対してアルバムへのアクセス許可を与えた場合、シームレスな連携が実現できる一方で、過剰な権限を与えている、とも言えます。postMessageで画像のURLを受け取る、という共通のインタフェースさえ定義されていれば、 どんなサービスであってもよいわけです。

OAuth 2.0との比較

OAuth 2.0の場合、Implicit Flowを使うことでクライアントサイドにのみ認可を与えることも可能です。しかし、Service Provider側では有効なアクセストークンとどのクライアントに対して認可が与えられたのかをやはり管理する必要があります。サーバ側とクライアント側、管理の役割は次のとおりです。

  • サーバ側:有効なアクセストークンの管理
  • クライアント側:取得したアクセストークンの管理

OAuthを使って、クライアントサイドにのみ認可を与えるということは実現できますが、Service Provider側では、ユーザがどんなアプリケーションを使っているのか把握できてしまうことになります[8]⁠。

対してpostMessageを使った方式のポイントは、Webサイト連携のための機能をその気になればJavaScriptだけで完結させることができるようになる、ということです。photo.example.comは連携機能を許可済みのドメインの一覧をphoto.example.comのlocalStorageに保存しておくことができます。blog.example.jpは、ユーザが使っている写真サービスの一覧を、blog.example.jpのlocalStorageに保存しておくことができます。連携しながらも、サービス事業者側では、お互いにどんなサービスを使っているのかすらわからない、という状態を作ることができます。

おすすめ記事

記事・ニュース一覧