1. ホーム
  2. javascript

Axiosインターセプターは元のリクエストを再試行し、元のプロミスにアクセスします。

2023-09-02 21:52:05

質問

アクセストークンが期限切れの場合、401エラーをキャッチするためにインターセプターを設置しています。有効期限が切れた場合、新しいアクセストークンを取得するためにリフレッシュトークンを試します。この間、他の呼び出しが行われた場合、アクセストークンが検証されるまでキューに入れられます。

これはすべて非常にうまく動作しています。しかし、Axios(originalRequest)を使用してキューを処理するとき、もともと添付されたプロミスは呼び出されません。例として、以下を参照してください。

動作するインターセプターのコードです。

Axios.interceptors.response.use(
  response => response,
  (error) => {
    const status = error.response ? error.response.status : null
    const originalRequest = error.config

    if (status === 401) {
      if (!store.state.auth.isRefreshing) {
        store.dispatch('auth/refresh')
      }

      const retryOrigReq = store.dispatch('auth/subscribe', token => {
        originalRequest.headers['Authorization'] = 'Bearer ' + token
        Axios(originalRequest)
      })

      return retryOrigReq
    } else {
      return Promise.reject(error)
    }
  }
)

リフレッシュ方法(リフレッシュトークンを使って、新しいアクセストークンを取得する)

refresh ({ commit }) {
  commit(types.REFRESHING, true)
  Vue.$http.post('/login/refresh', {
    refresh_token: store.getters['auth/refreshToken']
  }).then(response => {
    if (response.status === 401) {
      store.dispatch('auth/reset')
      store.dispatch('app/error', 'You have been logged out.')
    } else {
      commit(types.AUTH, {
        access_token: response.data.access_token,
        refresh_token: response.data.refresh_token
      })
      store.dispatch('auth/refreshed', response.data.access_token)
    }
  }).catch(() => {
    store.dispatch('auth/reset')
    store.dispatch('app/error', 'You have been logged out.')
  })
},

auth/actionsモジュールのSubscribeメソッド。

subscribe ({ commit }, request) {
  commit(types.SUBSCRIBEREFRESH, request)
  return request
},

Mutationと同様に

[SUBSCRIBEREFRESH] (state, request) {
  state.refreshSubscribers.push(request)
},

以下はアクションの例です。

Vue.$http.get('/users/' + rootState.auth.user.id + '/tasks').then(response => {
  if (response && response.data) {
    commit(types.NOTIFICATIONS, response.data || [])
  }
})

このリクエストがキューに追加された場合、リフレッシュトークンが新しいトークンにアクセスしなければならないので、私はオリジナルのthen()を添付したいと思います。

  const retryOrigReq = store.dispatch('auth/subscribe', token => {
    originalRequest.headers['Authorization'] = 'Bearer ' + token
    // I would like to attache the original .then() as it contained critical functions to be called after the request was completed. Usually mutating a store etc...
    Axios(originalRequest).then(//if then present attache here)
  })

アクセストークンがリフレッシュされると、リクエストのキューが処理されます。

refreshed ({ commit }, token) {
  commit(types.REFRESHING, false)
  store.state.auth.refreshSubscribers.map(cb => cb(token))
  commit(types.CLEARSUBSCRIBERS)
},

どのように解決するのですか?

2019年2月13日更新

このトピックに多くの人が興味を示しているので、私は、このトピックのために axios-auth-refresh パッケージを作成しました。 を作成しました。これは、ここで指定された動作を実現するのに役立つはずです。


ここで重要なのは、正しいPromiseオブジェクトを返すことであり、そのために .then() でチェーニングすることです。そのためにVuexのステートを使うことができます。リフレッシュの呼び出しが発生した場合、その時点で refreshing の状態を true に変更すれば、保留中の呼び出しに更新を設定することもできます。このように .then() を使用すると、常に正しいPromiseオブジェクトにバインドされ、Promiseが完了したときに実行されます。このようにすることで、トークンの更新を待っている呼び出しを保持するための余分なキューが不要になります。

function refreshToken(store) {
    if (store.state.auth.isRefreshing) {
        return store.state.auth.refreshingCall;
    }
    store.commit('auth/setRefreshingState', true);
    const refreshingCall = Axios.get('get token').then(({ data: { token } }) => {
        store.commit('auth/setToken', token)
        store.commit('auth/setRefreshingState', false);
        store.commit('auth/setRefreshingCall', undefined);
        return Promise.resolve(true);
    });
    store.commit('auth/setRefreshingCall', refreshingCall);
    return refreshingCall;
}

これは常に、すでに作成されたリクエストをPromiseとして返すか、新しいリクエストを作成し、他の呼び出しのためにそれを保存するかのどちらかでしょう。これで、あなたのインターセプターは次のようなものになるでしょう。

Axios.interceptors.response.use(response => response, error => {
    const status = error.response ? error.response.status : null

    if (status === 401) {

        return refreshToken(store).then(_ => {
            error.config.headers['Authorization'] = 'Bearer ' + store.state.auth.token;
            error.config.baseURL = undefined;
            return Axios.request(error.config);
        });
    }

    return Promise.reject(error);
});

これにより、保留されているすべてのリクエストをもう一度実行することができます。しかし、クエリなしですべて一度に実行されます。


保留中のリクエストを実際に呼び出された順に実行させたい場合は、コールバックを第二引数として refreshToken() 関数の2番目のパラメータとして渡す必要があります。

function refreshToken(store, cb) {
    if (store.state.auth.isRefreshing) {
        const chained = store.state.auth.refreshingCall.then(cb);
        store.commit('auth/setRefreshingCall', chained);
        return chained;
    }
    store.commit('auth/setRefreshingState', true);
    const refreshingCall = Axios.get('get token').then(({ data: { token } }) => {
        store.commit('auth/setToken', token)
        store.commit('auth/setRefreshingState', false);
        store.commit('auth/setRefreshingCall', undefined);
        return Promise.resolve(token);
    }).then(cb);
    store.commit('auth/setRefreshingCall', refreshingCall);
    return refreshingCall;
}

そして、インターセプター。

Axios.interceptors.response.use(response => response, error => {
    const status = error.response ? error.response.status : null

    if (status === 401) {

        return refreshToken(store, _ => {
            error.config.headers['Authorization'] = 'Bearer ' + store.state.auth.token;
            error.config.baseURL = undefined;
            return Axios.request(error.config);
        });
    }

    return Promise.reject(error);
});

2番目の例はまだテストしていませんが、うまくいくか、少なくともアイデアを与えてくれるはずです。

最初の例の動作デモ - モックリクエストとそれに使われるサービスのデモ版のため、しばらくすると動かなくなりますが、それでもコードは存在します。

ソースはこちら。 インターセプター - インターセプトされたメッセージがエラーとして解決されるのを防ぐ方法