web-dev-qa-db-ja.com

Axios Interceptorsは元のリクエストを再試行し、元のプロミスにアクセスします

アクセストークンの有効期限が切れた場合に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 ({ commit }, request) {
  commit(types.SUBSCRIBEREFRESH, request)
  return request
},

突然変異と同様に:

[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 || [])
  }
})

このリクエストがキューIに追加された場合、更新トークンが新しいトークンにアクセスする必要があるため、元の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)
},
16
Tim Wickstrom

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番目の例はテストしていませんが、動作するか、少なくともアイデアが得られるはずです。

最初の例の動作デモ -モック要求とそれらに使用されるサービスのデモバージョンのため、しばらく経っても動作しませんが、それでもコードは存在します。

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

38
Dawid Zbiński

このようなものを試してみませんか?

ここでは、両方向でAXIOSインターセプターを使用します。発信方向には、Authorizationヘッダーを設定します。入力方向について-エラーがある場合、私は約束を返します(AXIOSはそれを解決しようとします)。 Promiseはエラーが何であるかをチェックします-401であり、それが初めて表示された場合(つまり、再試行の範囲内にない場合)、トークンを更新しようとします。そうでなければ、元のエラーをスローします。私の場合、refreshToken()はAWS Cognitoを使用しますが、最も適したものなら何でも使用できます。ここに、refreshToken()の2つのコールバックがあります:

  1. トークンが正常に更新されたら、更新された構成を使用してAXIO​​Sリクエストを再試行します。新しい新鮮なトークンを含め、retryフラグを設定して、APIが401エラーで繰り返し応答する場合に無限のサイクルに入らないようにします。 resolveおよびreject引数をAXIOSに渡す必要があります。そうしないと、新しいプロミスが解決または拒否されません。

  2. 何らかの理由でトークンを更新できなかった場合は、約束を拒否します。 AWS Cognito内のコールバックの周りにtry/catchブロックが存在する可能性があるため、単純にエラーをスローすることはできません


Vue.prototype.$axios = axios.create(
  {
    headers:
      {
        'Content-Type': 'application/json',
      },
    baseURL: process.env.API_URL
  }
);

Vue.prototype.$axios.interceptors.request.use(
  config =>
  {
    events.$emit('show_spin');
    let token = getTokenID();
    if(token && token.length) config.headers['Authorization'] = token;
    return config;
  },
  error =>
  {
    events.$emit('hide_spin');
    if (error.status === 401) VueRouter.Push('/login'); // probably not needed
    else throw error;
  }
);

Vue.prototype.$axios.interceptors.response.use(
  response =>
  {
    events.$emit('hide_spin');
    return response;
  },
  error =>
  {
    events.$emit('hide_spin');
    return new Promise(function(resolve,reject)
    {
      if (error.config && error.response && error.response.status === 401 && !error.config.__isRetry)
      {
        myVue.refreshToken(function()
        {
          error.config.__isRetry = true;
          error.config.headers['Authorization'] = getTokenID();
          myVue.$axios(error.config).then(resolve,reject);
        },function(flag) // true = invalid session, false = something else
        {
          if(process.env.NODE_ENV === 'development') console.log('Could not refresh token');
          if(getUserID()) myVue.showFailed('Could not refresh the Authorization Token');
          reject(flag);
        });
      }
      else throw error;
    });
  }
); 
2
IVO GELOV