【问题标题】:Axios Interceptors retry original request and access original promiseAxios 拦截器重试原始请求并访问原始承诺
【发布时间】:2019-01-04 22:11:35
【问题描述】:

如果访问令牌过期,我有一个拦截器来捕获 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)
},

【问题讨论】:

  • 您无法获取“原始 .then() 回调”并将它们附加到您的新请求中。相反,您需要从拦截器返回一个新结果的 Promise,以便它使用新结果解析原始 Promise。
  • 我不了解 axios 或 vue 的详细信息,但会假设像 const retryOrigReq = store.dispatch('auth/subscribe').then(token => { originalRequest.headers['Authorization'] = 'Bearer ' + token; return Axios(originalRequest) }); 这样的东西应该这样做
  • 我更新了问题以添加更多上下文。我需要找到一种方法来运行原始请求中的 then 语句。在示例中,它会更新通知存储,例如。
  • 很高兴知道您的 subscribe 操作是什么样的,可能会有所帮助。
  • @TimWickstrom 是的,运行这些then 回调的唯一方法是解决get(…) 调用返回的承诺。 Afaics,拦截器回调的返回值提供了这种能力。

标签: javascript vue.js promise vuejs2 axios


【解决方案1】:

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

【讨论】:

  • 它返回发送给它的请求。
  • 我添加了一个工作示例。你可以看看。
  • 我必须在我的 Axios 拦截器中添加的一件事是检查 401 从 oath/refresh 方法返回。如果它返回 401,它将陷入循环。我对此的临时实现只是检查 if 语句中的路径:if(location.substring(location.length - 13, location.length) === 'login/refresh') { .. do something }
  • 我将区分调用的逻辑委托给this question。希望有人至少会提出一个想法。否则,如果问题得到解决,我会非常乐意将这个拦截器作为一个包提供。
  • @DawidZbiński 这真的很方便。谢谢你把它放在一起。
【解决方案2】:

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

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

【讨论】:

  • 我错过了return new Promise( (resolve,reject) => {//refresh code},我什至没有想到将resolvereject 解析为.then() 函数。不过,这个答案可以对您的代码进行更多解释。但是一个可靠的代码片段。
【解决方案3】:

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

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

【讨论】:

    猜你喜欢
    • 2020-02-12
    • 2020-12-14
    • 2019-09-21
    • 1970-01-01
    • 2020-03-15
    • 2021-04-22
    • 2015-10-19
    • 2021-06-16
    • 1970-01-01
    相关资源
    最近更新 更多