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 ({ 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)
},

原文由 Tim Wickstrom 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 619
2 个回答

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() 函数,就像这样。

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

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

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

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

原文由 Dawid Zbiński 发布,翻译遵循 CC BY-SA 4.0 许可协议

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

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

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

  2. 如果由于任何原因无法刷新令牌 - 我们拒绝承诺。我们不能简单地抛出一个错误,因为可能有 try/catch 阻塞 AWS 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;
    });
  }
);

原文由 IVO GELOV 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题