[HTML5] 異なるオリジンのWebStorageの内容を取得する

WebStorage(localStorageとsessionStorage)は同一オリジン内のデータしか読み書きできません。名前空間を分けることでデータの衝突を防いだり、大切なデータを他のサイトから覗き見られることから守ってくれています。

しかしWebサービスを開発していると、サブドメイン間などでWebStorage内のデータを共有したいことがあります。そんなときに使うのが window.postMessage。指定したオリジンにだけWebStorage内のデータを渡したり、逆にデータを受け取ってWebStorage内に保存するといったドメインをまたいでの通信が可能になります。

同一オリジンとは「スキーム(http)」「ホスト(example.com)」「ポート番号(443)」のすべてが同じ場合を言います。詳しくはこちらをどうぞ

基本的な原理

図を見るとちょっと複雑に映りますが原理は簡単です。

  1. WebStorage内のデータを保存しているサイトをiframeで読み込む
  2. iframeに対してpostMessageでデータを要求
  3. iframe内でデータを取り出し要求元にpostMessageでデータを返信
  4. iframeからデータを受信

ソースコード

データを保存している側

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf8">
  <title>example.com/a.html</title>
</head>
<body>

<h1>example.com/a.html</h1>

<script>
window.addEventListener('load', (e)=>{
  // 実験用のデータをWebStorageに保存。
  // もちろん同一オリジン内であれば別のページで記録してもOK
  try{
    const data = JSON.stringify({id:1, name:'foo', age:16})
    localStorage.setItem('user', data)
  }
  catch(e){
    console.error(e)
  }
})

/**
 * メッセージ処理
 */
window.addEventListener('message', (e)=>{
  // 通信元のoriginをチェック
  if( e.origin === 'https://katsubemakito.net' ) {
    const command = e.data  // 送信元からのデータ(要求)を受け取る

    // 送信元の要求に従って処理
    switch(command){
      case 'GET':
        // WebStorage内のデータ読み込み
        const buff = localStorage.getItem('user')
        const json = JSON.parse(buff)

        // 送信元へデータを返却
        e.source.postMessage({status:true, cmd:'GET', body:json}, e.origin)
        break;

      case 'DELETE':
        // WebStorage内のデータ削除
        localStorage.removeItem('user')

        // 送信元へ結果を返却
        e.source.postMessage({status:true}, cmd:'DELETE', e.origin)
        break;

      default:
        e.source.postMessage({status:false, cmd:command, error:'Unknown Command'}, e.origin)
        break;        
    }
  }
})
</script>
</body>
</html>

データを要求する側

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf8">
  <title>katsubemakito.net/b.html</title>
</head>
<body>

<h1>katsubemakito.net/b.html</h1>

<!-- 上記のファイルをiframeで読み込む -->
<iframe id="databridge" src="https://example.com/a.html" style="display:none"></iframe>

<script>
window.addEventListener('load', (e){
  const databridge = document.getElementById('databridge')

  /**
   * iframeの読み込みが完了した時点でデータを要求
   */
  databridge.addEventListener('load', ()=>{
    databridge.contentWindow.postMessage('GET', '*')  // 'DELETE'にすると削除できる
  })

  /**
   * iframe側から送信されたデータを受信
   */
  window.addEventListener('message', (e){
    const data = e.data
    console.log(data);
  })
})
</script>
</body>
</html>

サーバの準備

iframeで異なるオリジンはブロックされる場合がある

前述のコードを適当にサーバに置けば動きそうな気がしていたのですが、ローカルで開発用のサーバを立てて検証しているとブラウザ(Chrome)が以下のエラーを吐きiframeで指定したページを読み込んでくれませんでした。

SecurityError: Blocked a frame with origin http://localhost:3000 from accessing a cross-origin frame.

というわけでContent-Security-PolicyヘッダをWebサーバから返却するように設定を行います。 ※この設定をしなくても動くことがあるのですが……何なんでしょうね。

具体的な設定

Apache

DirectoryやFilesMatchなどで対象とするファイルに設定します。headers_moduleは大抵の場合最初から入っています。

<IfModule headers_module>
  <Directory "/var/www/html/">
    Header always set Content-Security-Policy "frame-src https://katsubemakito.net https://example.net"
  </Directory>
</IfModule>

nginx

Apacheと同様でconfファイルの適当な場所でadd_headerしてやります。

location ~ foo\.html$ {
  add_header Content-Security-Policy "frame-src https://katsubemakito.net https://example.net";
}

AWS CloudFront

対象のファイルにだけビヘイビアを追加して設定する感じです。

レスポンスヘッダーの設定を行う項目で新規にポリシーを作成します。

ポリシーの作成画面でContent-Security-Policyを設定すれば完了です。

ブラウザの対応状況

そうは言ってもWebブラウザが対応してなければ意味無いわけですが、みんな大好きCan I Useによると、ほぼほぼ気にしなくて良さそうですね。

参考ページ