Javascript Axios 拦截器重试原始请求并访问原始承诺

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/51563821/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-23 04:51:47  来源:igfitidea点击:

Axios Interceptors retry original request and access original promise

javascriptvue.jspromisevuejs2axios

提问by Tim Wickstrom

I have an interceptor in place to catch 401 errors if the access token expires. If it expires it tries the refresh token to get a new access token. If any other calls are made during this time they are queued until the access token is validated.

如果访问令牌过期,我有一个拦截器来捕获 401 错误。如果它过期,它会尝试刷新令牌以获取新的访问令牌。如果在此期间进行任何其他调用,则它们将排队等待访问令牌得到验证。

This is all working very well. However when processing the queue using Axios(originalRequest) the originally attached promises are not being called. See below for an example.

这一切都运作良好。但是,在使用 Axios(originalRequest) 处理队列时,不会调用最初附加的承诺。请参阅下面的示例。

Working interceptor code:

工作拦截器代码:

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 Method (Used the refresh token to get a new access token)

刷新方法(使用刷新令牌获取新的访问令牌)

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.')
  })
},

Subscribe method in auth/actions module:

auth/actions 模块中的订阅方法:

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

As well as the Mutation:

以及突变:

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

Here is a sample action:

这是一个示例操作:

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

If this request was added to the queue I because the refresh token had to access a new token I would like to attach the original then():

如果这个请求被添加到队列中,我因为刷新令牌必须访问一个新令牌,我想附加原始 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)
  })

Once the access token has been refreshed the queue of requests is processed:

刷新访问令牌后,将处理请求队列:

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

回答by Dawid Zbiński

Update Feb 13, 2019

2019 年 2 月 13 日更新

As many people have been showing an interest in this topic, I've created the axios-auth-refresh packagewhich should help you to achieve behaviour specified here.

由于许多人对这个主题表现出兴趣,我创建了axios-auth-refresh 包,它应该可以帮助您实现此处指定的行为。



The key here is to return the correct Promise object, so you can use .then()for chaining. We can use Vuex's state for that. If the refresh call happens, we can not only set the refreshingstate to true, we can also set the refreshing call to the one that's pending. This way using .then()will always be bound onto the right Promise object, and be executed when the Promise is done. Doing it so will ensure you don't need an extra queue for keeping the calls which are waiting for the token's refresh.

这里的关键是返回正确的 Promise 对象,以便您可以.then()用于链接。为此,我们可以使用 Vuex 的状态。如果发生刷新调用,我们不仅可以将refreshing状态设置为true,还可以将刷新调用设置为挂起的那个。这种方式 using.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;
}

This would always return either already created request as a Promise or create the new one and save it for the other calls. Now your interceptor would look similar to the following one.

这将始终返回已创建的请求作为 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);
});

This will allow you to execute all the pending requests once again. But all at once, without any querying.

这将允许您再次执行所有挂起的请求。但是一下子,没有任何询问。



If you want the pending requests to be executed in the order they were actually called, you need to pass the callback as a second parameter to the refreshToken()function, like so.

如果您希望挂起的请求按照它们实际调用的顺序执行,您需要将回调作为第二个参数传递给refreshToken()函数,就像这样。

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;
}

And the interceptor:

和拦截器:

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);
});

I haven't tested the second example, but it should work or at least give you an idea.

我还没有测试过第二个例子,但它应该可以工作,或者至少给你一个想法。

Working demo of first example- because of the mock requests and demo version of service used for them, it will not work after some time, still, the code is there.

第一个示例的工作演示- 由于模拟请求和用于它们的服务的演示版本,一段时间后它将无法工作,但代码仍然存在。

Source: Interceptors - how to prevent intercepted messages to resolve as an error

来源:拦截器 - 如何防止拦截的消息解析为错误

回答by IVO GELOV

Why not try something like this ?

为什么不尝试这样的事情呢?

Here I use AXIOS interceptors in both directions. For the outgoing direction I set the Authorizationheader. For the incoming direction - if there is an error, I return a promise (and AXIOS will try to resolve it). The promise checks what the error was - if it was 401 and we see it for the first time (i.e. we are not inside the retry) then I try to refresh the token. Otherwise I throw the original error. In my case refreshToken()uses AWS Cognito but you can use whatever suits you most. Here I have 2 callbacks for refreshToken():

这里我在两个方向都使用 AXIOS 拦截器。对于传出方向,我设置了Authorization标题。对于传入方向 - 如果出现错误,我会返回一个承诺(并且 AXIOS 将尝试解决它)。承诺检查错误是什么 - 如果它是 401 并且我们第一次看到它(即我们不在重试中)然后我尝试刷新令牌。否则我会抛出原始错误。就我而言,refreshToken()使用 AWS Cognito,但您可以使用最适合您的任何方式。在这里,我有 2 个回调refreshToken()

  1. when the token is successfully refreshed, I retry the AXIOS request using an updated config - including the new fresh token and setting a retryflag so that we do not enter an endless cycle if the API repeatedly responds with 401 errors. We need to pass the resolveand rejectarguments to AXIOS or otherwise our fresh new promise will be never resolved/rejected.

  2. if the token could not be refreshed for any reason - we reject the promise. We can not simply throw an error because there might be try/catchblock around the callback inside AWS Cognito

  1. 当令牌成功刷新时,我使用更新的配置重试 AXIOS 请求 - 包括新的新令牌并设置一个retry标志,这样如果 API 反复响应 401 错误,我们就不会进入无限循环。我们需要将resolvereject参数传递给 AXIOS,否则我们的新承诺将永远不会被解决/拒绝。

  2. 如果令牌因任何原因无法刷新 - 我们拒绝承诺。我们不能简单地抛出错误,因为try/catchAWS Cognito 内部的回调可能存在阻塞



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;
    });
  }
);