web-dev-qa-db-ja.com

axiosのインターセプターを介したアクセストークンの更新の自動化

最近、OAuth認証トークンの更新 この質問 のaxiosインターセプターについて説明しました。

基本的に、インターセプターは401ステータスコードで応答をインターセプトし、トークンを更新しようとします。それを念頭に置いて、次に行うことは、インターセプターからPromiseを返すことです。これにより、通常は失敗する要求は、トークンの更新後に何も起こらないように実行されます。

主な問題は、インターセプタが401ステータスコードのみをチェックすることです。これは十分ではありません。refreshTokenも失敗すると401ステータスコードを返します。ループ。

私が考えているシナリオは2つあります。

  1. 呼び出されたURLを確認します。したがって、/auth/refreshである場合、トークンを更新しようとするべきではありません。
  2. refreshTokenロジックが呼び出されたときにインターセプターを省略します

最初のオプションは、少し「動的ではない」ように見えます。 2番目のオプションは有望に見えますが、可能かどうかはわかりません。

主な質問は、インターセプターで呼び出しを区別/識別し、具体的に「ハードコーディング」せずに異なるロジックを実行する方法、または指定されたコールのインターセプターを省略する方法はありますか?前もって感謝します。

インターセプターのコードは、質問の理解に役立つ場合があります。

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

    if (status === 401) {
        // will loop if refreshToken returns 401
        return refreshToken(store).then(_ => {
            error.config.headers['Authorization'] = 'Bearer ' + store.state.auth.token;
            error.config.baseURL = undefined;
            return Axios.request(error.config);
        })
        // Would be Nice to catch an error here, which would work, if the interceptor is omitted
        .catch(err => err);
    }

    return Promise.reject(error);
});

トークン更新部分:

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;
}
9
Dawid Zbiński

これを処理するはるかに簡単な方法を見つけたかもしれません:/ api/refresh_tokenエンドポイントを呼び出したときにaxios.interceptors.response.eject()を使用してインターセプターを無効にし、その後再度有効にします。

コード :

createAxiosResponseInterceptor() {
    const interceptor = axios.interceptors.response.use(
        response => response,
        error => {
            // Reject promise if usual error
            if (errorResponse.status !== 401) {
                return Promise.reject(error);
            }

            /* 
             * When response code is 401, try to refresh the token.
             * Eject the interceptor so it doesn't loop in case
             * token refresh causes the 401 response
             */
            axios.interceptors.response.eject(interceptor);

            return axios.post('/api/refresh_token', {
                'refresh_token': this._getToken('refresh_token')
            }).then(response => {
                saveToken();
                error.response.config.headers['Authorization'] = 'Bearer ' + response.data.access_token;
                return axios(error.response.config);
            }).catch(error => {
                destroyToken();
                this.router.Push('/login');
                return Promise.reject(error);
            }).finally(createAxiosResponseInterceptor);
        }
    );
}
25
Ismoil Shifoev

これが要件に合っているかどうかはわかりませんが、別の回避策は別のAxiosインスタンス(axios.createメソッド)refreshTokenおよび残りのAPI呼び出し用。これにより、refreshTokenの場合に401ステータスを確認するためにデフォルトのインターセプターを簡単にバイパスできます。

したがって、通常のインターセプターは同じになります。

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

  if (status === 401) {
    // will loop if refreshToken returns 401
    return refreshToken(store).then(_ => {
      error.config.headers['Authorization'] = 'Bearer ' + store.state.auth.token;
      error.config.baseURL = undefined;
      return Axios.request(error.config);
    })
    // Would be Nice to catch an error here, which would work, if the interceptor is omitted
    .catch(err => err);
  }

  return Promise.reject(error);
});

そして、refreshTokenは次のようになります。

const refreshInstance = Axios.create();

function refreshToken(store) {
  if (store.state.auth.isRefreshing) {
    return store.state.auth.refreshingCall;
  }

  store.commit('auth/setRefreshingState', true);
  const refreshingCall = refreshInstance.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;
}

ここにいくつかの素敵なリンクがあります [1][2] 、Axiosインスタンスを参照できます

2
waleed ali

選択したソリューションで省略されているように思われるものは次のとおりです。更新中にリクエストがトリガーされるとどうなりますか?そして、なぜトークンが期限切れになり、401応答が新しいトークンを取得するのを待つのでしょうか?

1)更新要求がトリガーされます

2)通常のリソースに対する別の要求がトリガーされます

3)更新応答を受信し、トークンが変更されました(古いトークンが無効であることを意味します)

4)ステップ2からのリクエストをバックエンドで処理しますが、古いトークンを受け取りました=> 401

基本的に、リフレッシュリクエスト中に発生したすべてのリクエストに対して401を受け取ります(少なくとも私が直面している問題です)。

この質問から Ajax呼び出しが完了するまでAxios Request Interceptorは待つ で、この質問に対する@ waleed-ALiの回答から、要求インターセプターはPromiseを返すことができるようです。

私の解決策は、要求を保持し、更新要求が解決された直後にそれらを起動することです。

私のvuexストアのユーザーモジュール(vuex + vuex-module-decorators):

  @Action({ rawError: true })
  public async Login(userInfo: { email: string, password: string }) {
    let { email, password } = userInfo
    email = email.trim()
    const { data } = await login({ email, password })
    setToken(data.access_token)
    setTokenExpireTime(Date.now() + data.expires_in * 1000)
    this.SET_TOKEN(data.access_token)
    // after getting a new token, set up the next refresh in 'expires_in' - 10 seconds
    console.log("You've just been logged-in, token will be refreshed in ", data.expires_in * 1000 - 10000, "ms")
    setTimeout(this.RefreshToken, data.expires_in * 1000 - 10000)
  }

  @Action
  public async RefreshToken() {
    setRefreshing(refresh().then(({ data }) => {
      setToken(data.access_token) // this calls a util function to set a cookie
      setTokenExpireTime(Date.now() + data.expires_in * 1000) // same here
      this.SET_TOKEN(data.access_token)
      // after getting a new token, set up the next refresh in 'expires_in' - 10 seconds
      console.log('Token refreshed, next refresh in ', data.expires_in * 1000 - 10000)
      setTimeout(this.RefreshToken, data.expires_in * 1000 - 10000)
      setRefreshing(Promise.resolve())
    }))
  }

ログインアクションで、トークンの有効期限が切れる直前にRefreshTokenを呼び出すタイムアウトを設定します。

RefreshTokenアクションでも同じです。したがって、401が実行される前にトークンを自動的に更新する更新ループを作成します。

ユーザーモジュールの2つの重要な行は次のとおりです。

setRefreshing(Promise.resolve())

更新要求が満たされると、変数を更新すると即座に解決されます。

そして:

setRefreshing(refresh().then(({ data }) => {

これにより、api/user.tsファイルのrefreshメソッドが呼び出されます(これにより、axiosが呼び出されます)。

export const refresh = () =>
  request({
    url: '/users/login/refresh',
    method: 'post'
  })

返されたPromiseをutils.tsのsetRefreshingユーティリティメソッドに送信します。

let refreshing: Promise<any> = Promise.resolve()
export const getRefreshing = () => refreshing
export const setRefreshing = (refreshingPromise: Promise<any>) => { refreshing = refreshingPromise }

更新変数はデフォルトで解決済みのPromiseを保持し、起動時に保留中の更新要求に設定されます。

次にrequest.tsで:

    service.interceptors.request.use(
  (config) => {
    if (config.url !== '/users/login/refresh') {
      return getRefreshing().then(() => {
        // Add Authorization header to every request, you can add other custom headers here
        if (UserModule.token) {
          console.log('changing token to:', UserModule.token)
          console.log('calling', config.url, 'now')
          config.headers['Authorization'] = 'Bearer ' + UserModule.token
        }
        return config
      })
    } else {
      return Promise.resolve(config)
    }
  },
  (error) => {
    Promise.reject(error)
  }
)

リクエストがリフレッシュエンドポイントに対するものである場合、すぐに解決し、そうでない場合は、更新されたトークンを取得した後、リフレッシュプロミスを返し、インターセプターでやりたいことと連鎖させます。現在保留中の更新要求がない場合は、すぐに解決するように約束が設定されます。更新要求がある場合は、それが解決するまで待機し、新しいトークンで他のすべての保留中の要求を起動できます。

更新エンドポイントを無視するようにインターセプターを構成するだけで改善できますが、その方法はまだわかりません。

0
user3803848