axiosで認証切れエラー起こしたリクエストを再現してリカバリーする

こんにちは。

業務委託としてHRBrainの開発に関わっている川口です。
(サイバー時代の同僚が多いのでCTOにはなぜかタメ口だったりしますなんかすみません)

この記事は HRBrainAdventCalendar 10日目の記事です。

主にHRBrainではフロントエンドエンジニアとして関わっていまして、今回のアドベントカレンダーではaxiosの再リクエストに関する記事を書いていきます。

よろしくお願いします。

f:id:hrb-goodtea0223:20201207174456p:plain

認証切れで全てが消えてしまう

たくさんの入力項目があるフォームで時間をかけて丁寧に入力したのに、保存直前に認証切れを起こして全てが消えてしまう。

そんなことありますよね。

HRBrainのサービスでもそういう状況が多々報告されたため、ユーザビリティを考慮した上でシームレスにストレス無く再認証してリクエストし直す、というような実装を進めました。

今回はその方法について紹介します。

方針

  • 実装はaxios内部のみで完結させる
  • 再認証は iFrame + postMessage で行いますが詳しくは解説しません(本記事と別の趣旨になるため)
  • 失敗したリクエストのconfigを保持しておき、再認証後にそれを使って同一のリクエストを実行する

確認した環境

  • TypeScript
  • axios@0.21.0

コード

以下のようにaxiosのインスタンスにresponseのinterceptorを設定します。

import Axios, { AxiosResponse, AxiosRequestConfig } from 'axios'

const instance = Axios.create({ withCredentials: true })

const handleExpiredTokenRejectedInterceptor = async (error: any) => {
  const { response }: { response: AxiosResponse } = error
  const statusCode = response?.status

  // 401の認証切れエラーの場合にのみ実行
  if (statusCode === 401) {
    const { config } = error
    // 再認証を実行し、それが完了した後に再度同一のリクエストを実行
    return tryReLogin().then(() => axios.request(config))
  }

  return Promise.reject(error)
}

let reLoginResolves: ((value?: unknown) => void)[] = []

function tryReLogin() {
  return new Promise((resolve) => {
    // 一回目のみ再認証処理を実行
    if (!reLoginResolves.length) {
      // ここでredux等を用いて画面上にモーダルを表示し、その中のiFrameで認証ページを表示する
      // dispatch(actions.isReLogin(true))

      // iFrame上の認証ページ内で認証完了したタイミングでpostMessageし、ここで受け取る
      window.addEventListener(
        'message',
        function handleMessage(event) {
          // ここでresolveを実行すると失敗していたリクエストが再実行される
          reLoginResolves.forEach((resolve) => resolve())

          window.removeEventListener('message', handleMessage)
          reLoginResolves = []
        },
        false
      )
    }

    reLoginResolves.push(resolve)
  })
}

instance.interceptors.response.use(undefined, handleExpiredTokenRejectedInterceptor)

ざっくり流れの解説

全体の流れとしてはざっくり以下のようになってます。

認証切れエラーを検知したら再認証用のiFrameログインモーダルを表示
 ↓
ログイン完了のタイミングでiFrame側からpostMessageを受け取る
 ↓
受け取りタイミングでiFrameログインモーダルを消し、貯めておいた失敗リクエストをまとめて実行
 ↓
失敗リクエストがそのまま再現されるため、認証エラーで失敗しかけていたページ表示が正常に行われる

詳細な解説

handleExpiredTokenRejectedInterceptor

axiosに設定しているInterceptor処理になります。
この中で認証エラーを検知してtryRelogin関数を実行し、再認証完了後に再度同一リクエストを実行しています。

tryReLogin

エラーが発生するたびに実行される処理ですが、再認証用のiFrameログインモーダルの表示は初回のみに実行されます。
postMessageの受け取りイベントもここで定義しており、認証が完了したタイミングでreLoginResolvesに貯めていたresolveをまとめて実行することによって失敗したリクエストが再度実行されます。

再認証用のiFrameログインモーダル

初回のtryReLoginで表示しているものです。
HRBrainではサービスと認証が別環境になっているため、iFrameとpostMessageを利用してシームレスな再認証を実現しています。
postMessageは認証完了タイミングの通知をしているのみですが、環境によってはpostMessageに必要な情報を乗せてサービス側に渡しても良いかもしれません。
その辺りは各自のサービスに合わせて調整いただければ。

まとめ

普段はそのままログイン画面にリダイレクトしがちな再認証処理ですが、今回の手法はいかがだったでしょうか。
他にもLocalStorageに入力途中の値を保存しておいてログイン画面にリダイレクトする、などの方法も取れますが、ユーザーの不安も考えると(リダイレクトによって入力値が消えていないか)今回紹介したような方法がベストなように思います。
よければ再認証の際の一つの方法として参考にしていただければ幸いです。