Axios拦截器重试原始请求并访问原始承诺 [英] Axios Interceptors retry original request and access original promise

查看:89
本文介绍了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)
},

解决方案

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

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

第一个示例的工作演示-由于使用了模拟请求和服务的演示版本,一段时间后仍然无法正常工作,

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

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.

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.

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:

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():

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

解决方案

Update Feb 13, 2019

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


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 refreshing state 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.

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.

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.

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

这篇关于Axios拦截器重试原始请求并访问原始承诺的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆