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

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

一切都很顺利。但是,当使用 Axios (OrigalRequest)处理队列时,不会调用最初附加的承诺。下面是一个例子。

工作拦截代码:

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 (使用刷新令牌获取新的访问令牌)

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

如果这个请求被添加到队列 I 中,因为刷新令牌必须访问一个新的令牌,那么() :

  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)
},
97209 次浏览

Why not try something like this ?

Here I use AXIOS interceptors in both directions. For the outgoing direction I set the Authorization header. 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():

  1. when the token is successfully refreshed, I retry the AXIOS request using an updated config - including the new fresh token and setting a retry flag so that we do not enter an endless cycle if the API repeatedly responds with 401 errors. We need to pass the resolve and reject arguments 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/catch block around the callback inside 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;
});
}
);

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

This could be done with a single interceptor:

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

The rest is an application specific code:

  • Login to api
  • Save/load auth data to/from storage
  • Refresh token

Check out the complete example.