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

时间:2018-07-27 18:44:34

标签: javascript vue.js promise vuejs2 axios

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

一切正常。但是,在使用Axios(originalRequest)处理队列时,不会调用原始附加的promise。参见下面的示例。

有效的拦截器代码:

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

以及变异:

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

3 个答案:

答案 0 :(得分:24)

更新2019年2月13日

由于许多人对此主题表现出兴趣,因此我创建了axios-auth-refresh package,它可以帮助您实现本主题中指定的行为。


此处的关键是返回正确的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()函数,

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

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

Working demo of first example-由于模拟请求和用于它们的服务的演示版本,因此一段时间后仍然无法正常工作,

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

答案 1 :(得分:2)

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

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

  1. 成功刷新令牌后,我使用更新的配置重试AXIOS请求-包括新的新鲜令牌并设置retry标志,这样,如果API反复出现,我们就不会进入无限循环响应401错误。我们需要将resolvereject参数传递给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 :(得分:1)

这可以通过一个拦截器来完成:

let _refreshToken = '';
let _authorizing: Promise<void> | null = null;
const HEADER_NAME = 'Authorization';

axios.interceptors.response.use(undefined, async (error: AxiosError) => {
    if(error.response?.status !== 401) {
        return Promise.reject(error);
    }

    // create pending authorization
    _authorizing ??= (_refreshToken ? refresh : authorize)()
        .finally(() => _authorizing = null)
        .catch(error => Promise.reject(error));

    const originalRequestConfig = error.config;
    delete originalRequestConfig.headers[HEADER_NAME]; // use from defaults

    // delay original requests until authorization has been completed
    return _authorizing.then(() => axios.request(originalRequestConfig));
});

其余的是应用程序特定代码:

  • 登录api
  • 在存储中保存/加载身份验证数据
  • 刷新令牌

查看complete example