SaaSのリダイレクト処理をiframe + postMessageを活用してメイン画面遷移の外で扱う

アソビューの井上です。
最近とあるSaaSと連携する機能を実装する際に、iframeとpostMessageを活用したのでその内容について個人的な備忘として、また社内外への実装共有も兼ねてまとめてみたいと思います。

前提

今回はアソビュー!のWeb(asoview.com)開発において、あるSaaSと連携する機能を実装する際、FE↔BE↔SaaSもしくはFE↔SaaS間で情報の受け渡しを行う必要がありました。
今回の連携実装では、下記の図のようにフロントエンドからSDKを活用して初期化処理や関数の実行、必要なパラメータを付与した特定パスへのアクセス、コールバックによるリダイレクトなどを行う必要がありました。

単純化すると下記のようなフローになります。

Saas連携フロー

課題

こちらのフローはSaaSの公式のフローではあるのですが、このまま実装に移すといくつかの課題があります。

SaaSがリダイレクトを前提とした連携仕様になっている

今回の最も大きな課題です。
SaaSのフローがリダイレクトしながら情報を連携する作りになっており、そのまま使おうとすると画面のフローが変わってしまいます。

リダイレクトフロー問題

複数の機能およびアプリケーションに導入が必要

また、今回の実装においては、同じ機能をアソビュー!Web内の複数箇所、および今後別のアプリケーションにも実装する予定があります。実装箇所に応じて設計や実装を変えると効率が悪く、メンテナンス性も下がってしまいます。
それぞれのアプリケーションでの実装の違いを吸収した設計が求められていました。

長期のエンハンス運用に耐えられる設計

今回開発する機能は事業のコアになる部分でもあり、長くメンテナンスされる可能性がある部分のため、拡張時にも複雑にならず、なるべくシンプルで普遍的な設計にする必要があります。

方針

前述の通り、同じ連携方式の実装を各アプリケーションにも導入しなくてはなりませんが、大きく分けると対象のアプリケーションがSPA(NextやReact Routerを活用したSingle Page Application)、MPA(Spring BootのMulti Page Application)の場合があり、それらの作りを吸収しなくてはなりません。

リダイレクトのフローが遷移の中に入ってくることによって双方とも課題があります。

MPAの場合

サーバーサイドセッションで情報を保持できますが、もともとパラメータで連携していた情報をセッションに移すとサーバーサイドの実装が肥大化したり、SPAで使うREST APIとの共通部分(例えばバリデーションやエラーパターンなど)の再設計などが追加で必要になったりします。

SPAの場合

クライアント遷移が分断されるため、情報をlocalStorageなど代替の場所に一時保存するか永続化するかなどを検討する必要があります。こちらもlocalStorageのライフサイクル(消すタイミングや生存期間など)といった新たな設計や実装が必要になります。

かといって、リダイレクトをまたがないように処理フローを変えるなどすると、既存の仕様やUXに大きく手を加える必要があり、現実的ではありません。

  • SPA、MPAで共通した作りにしたい
  • リダイレクト遷移前提のSaaSの仕様は変えられない
  • 新たなセッションアイテム、localStorageなど情報の保存場所を増やしたくない
  • 既存の処理の順番を大きく変えたくない、該当機能のUXを損ねたくない

これらのことから方針としては現在のページ遷移流れを崩さず、現在のページにSaaSのリダイレクトフローを組み込む とします。 これを実現するためiframeとpostMessageを活用する方針に決めました。

流れとしては下記です

iframe差し込みVer

iframeでSaaSからのリダイレクトを受け取り(#8)、親画面へ情報を通知する(#9)ことで、メイン画面の遷移は変えずにSaaSの仕様に従ったリダイレクトを受けながら実装できます。

実装内容

具体的な実装は下記のコードになります
※要点をわかりやすくするため、細かい実装は省きつつ抜粋しています。

iframeを表示するモーダル(疑似画面遷移用)

const CompletionFrameModal = ({
  isModalOpen,
  redirectUrl,
  onLoaded,
}) => {
  return (
    <FullScreenModalContents opened={isModalOpen}>
      {redirectUrl && (
        <iframe
          src={redirectUrl}
          width='100%'
          height='100%'
          style={{ border: 'none' }}
          onLoad={onLoaded}
        />
      )}
    </FullScreenModalContents>
  )
}

iframeを含んだフルスクリーンのモーダルです。フルスクリーンにすることで表示上は擬似的に画面遷移させることができます。

呼び出し元では下記のような形でredirectUrlをpropsで渡し、iframeのonLoadにモーダルを開く関数を渡すことで、iframe呼び出し後にモーダルを開けるようにしています。(開閉を外からも操作したいのでprops経由で渡しています)

<CompletionFrameModal
  isModalOpen={isModalOpen}
  redirectUrl={redirectUrl}
  onLoaded={() => {
    openFrameModal()
  }}
/>

リダイレクトを受け取るページの処理

SaaS側にはあらかじめリダイレクトして戻ってくるアソビュー側の画面のURLを指定しており、SaaS内での諸々の処理が完了するとそこにパラメータを付けて戻るようになっています。下記が戻り先のアソビュー画面の実装です。
BEから受け取ったパラメータをpostMessageを用いて同一ドメインのメイン画面に送信し、メイン画面で受け取った変数を元にして処理を実行します。return nullのため、何も表示されません。
つまりこの「画面」はpostMessageを使って元画面にメッセージを送る役割のみを担っています。

const AuthFrame = () => {
  // バックエンドから受け取るパラメータ
  const { result, resultReason } =
    useDataSourceContext<DataSource>()

  useEffect(() => {
    if (result && resultReason) {
      window.parent.postMessage(
        { result, resultReason },
        location.origin
      )
    }
  }, [result, resultReason])

  return null
}

呼び出し元画面コンポーネントでイベントリスナーを設定するuseEffect

useEffect(() => {
    const messageHandler = (
      event: MessageEvent<{
        result: string
        resultReason: string
      }>
    ) => {
      if (event.origin !== location.origin) {
        // 信頼できるオリジンかどうかを検証
        return
      }

      const data = event.data
      if (data.result === 'success') {
        // iframeから認証成功の通知が来た場合に行う処理

      } else if (data.result === 'failure') {
        // iframeから認証失敗の通知が来た場合に行う処理

      }
    }

    window.addEventListener('message', messageHandler)
    return () => {
      window.removeEventListener('message', messageHandler)
    }
  }, [])

こちらでiframeで開いた画面から送信されたmessageイベントに対するイベントリスナーを設定し、通知を受けた後に処理を続行します。
window.postMessage は クロスオリジン通信を可能にする仕組みであり同じタブ内のiframeやポップアップなどのメッセージも届いてしまうため、信頼できるオリジンかどうかの検証は必要です。

結果

iframe上でリダイレクトを処理し、postMessageを通じて元画面で処理継続する作戦はうまくいきました!
UXを損なうこともなく、設計はSPA、MPAやアプリケーションごとの違いはほとんど無くそのまま適用できるような実装になったと思います。(モジュールとしての共通化まではしていないので実装ルールに従って各アプリケーション実装する形式です)

クライアントサイドの実装は比較的想定どおりに行ったのですが、一方クロスドメインのcookieの仕様がブラウザごとに違ったり、Spring Bootのiframeヘッダー仕様などサーバー連携の際にはいくつか課題がありました。
iframeをプロダクションで使う際は、複数端末、ブラウザなどの観点のテストが必須です。

まとめ

今回の機能は実は同様の機能を実現していた別のSaaSからの移行で、そのSaaSでは実装として実現できていた(SDKの中でiframeやpostMessageの活用を完結)ため同じことがスクラッチでも理論的にはできるはずということで検証しながら実装しました。

今回SaaSの開発担当者にも設計の妥当性など確認しながら開発できましたが、クロスドメイン観点などでSaaS側が対応しているかどうか・セキュリティ設定次第では使えない場合もありますので、注意が必要です。

スピード感が求められていたプロジェクトでしたが、結果的には今回の工夫をすることにより、短期的なデリバリーも叶え、中長期的にも全体の効率化と品質向上につながったものと思います。

最後に

アソビュー株式会社では新しいメンバーを随時募集していますので、ご興味ある方はお気軽にカジュアル面談ご応募ください。

www.asoview.com

speakerdeck.com