【JavaScript】別ドメインのlocalStorageを、CORS回避しながら取得できるか?

サイト移転で少し困った時のおはなし。

あるドメインに保管されているlocalStorageの情報を、別のドメインから取得したい。

詳細

※具体的な事例なので理解の妨げにならないのであれば飛ばして読んでもらってもOK。

元のドメイン(xxx.com)のサブドメイン(sub.xxx.com)でWebアプリケーションが配信されていて、情報の保存にlocalStorageを利用している。

↑これ。

また、親ドメインの方にそのWebアプリに関連する別ページが公開されている。このページはWebアプリの状況を参照するために、サブドメインのlocalStorageを取得して処理している。

ここで、親ドメインを移転する(xxx.com→yyy.com)。

アプリのページが元のドメインのままで関連ページのドメインだけが変わったため、fetch要求は当然弾かれる。CORSポリシーに引っかかってlocalStorageが取得できないために関連ページの機能が停止した……というのが大まかな流れ。

サブドメインも一緒に移行しないの? という声が当然のように聞こえてきそうだが、今回のケースではサブドメインがなぜか自分の管理下にないので(この辺はいろいろ事情があるので、まあ、察してください)、サブドメインのAccess-Control-Allow-Originにyyy.comを指定することができない。かといって丸ごと移植するのは予期しないエラーを吐かれそうでかなり怖い。

動いてるならそのままにしておきたい。触らぬ神に祟りなし。まあ、よくある話ですよね。

結論 – どちらのドメインも自分の管理下にあればなんとかなる

プロキシサーバーじゃないけど、機能的にはそれに近いものを用意してやり過ごした、というのが実情。

移転元親ドメイン(xxx.com)にfetch専用のアプリケーションをアップロードする。

  • アプリ内iframeでサブドメインのページも開く(そうしないとlocalStorageが読み込まれないため)。
  • 移転元親ドメインの方にAccess-Control-Allow-Originにyyy.comを指定する(重要)。これによってこのアプリがyyy.comから開け、情報の受け渡しが可能になる
  • アプリを開くとsub.xxx.comにfetch要求し、responseがあったらその内容を丸ごとopener(後述)にsubmitするように設計する

これをyyy.comからポップアップウィンドウで開く(普通に開いたらページ遷移しちゃうので)。ポップアップウィンドウはopener(そのウィンドウを開くトリガーになった親ページ)を情報として持てるので、無事にsub.xxx.comの情報をyyy.comまで運ぶことができる。

図に起こしてみると「なんでそんなことになってん?」って感じがしますね

サンプルコード

  const fetchDataAndSend = () => {
    if (loading) return;
    setLoading(true);

    window.addEventListener("message", handleMessage);

    const popup = window.open(IFRAME_ORIGIN, "popupWindow", "width=500,height=500");

    if (!popup) {
      alert("ポップアップがブロックされました");
      return;
    }
  };
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title></title>
  <script type="text/javascript">
    const origin = "https://xxx.com";

//onloadでサブドメインにfetch要求する
    window.addEventListener("load", () => {
      var iframe = document.querySelector("#subdomain-iframe");
      var iframeWindow = iframe.contentWindow;
      const dialog = document.getElementById("dialog");

      try {
        iframeWindow.postMessage("get", origin);
      } catch (e) {
        console.error("failed get data from iframe:", e);
        dialog.innerText = "処理に失敗しました";
      }
    });

//サブドメインからmessageが返ってきたらトリガーする処理
    window.addEventListener("message", function (event) {
      const dialog = document.getElementById("dialog");
      if (event.origin === origin) {
        try {
          let data = JSON.parse(event.data);
          if (data && typeof data === "object") {
            sendDataToParent(data);
          }
        } catch (e) {
          console.error("received illegal data:", event.data);
          dialog.innerText = "処理に失敗しました";
          return;
        }
      }
    });

//移転先のドメインに送る
    const sendDataToParent = (data) => {
      const dialog = document.getElementById("dialog");
      if (window.opener) {
        window.opener.postMessage(data, "https://yyy.com/receiver");
        dialog.innerText = "この画面は閉じてください。元の画面で引き続き処理を実行しています…";
      } else {
        console.log("couldnt find opener");
        dialog.innerText = "処理に失敗しました";
      }
    };
  </script>
</head>
<body>
  <p id="dialog">処理中です…</p>
  <iframe id="subdomain-iframe" src="https://sub.xxx.com" style="display: none;"></iframe>
</body>
</html>

当然ながら、xxx.com自体が自分の管理下にないならこの方法は使えない。と言うかその場合はもろにCORSを悪意を持って回避しようとしているケースなので、ここでは扱わない。

サブドメインが自分の管理下にないというのがなかなか想像しづらいので多分ほとんどの人には役に立たない例だけど、プロキシアプリを作るという考え方自体は応用が効きそう。

以上、備忘録でした。