如果访问令牌过期,我有一个拦截器来捕获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)
},
答案 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()
的回调:
成功刷新令牌后,我使用更新的配置重试AXIOS请求-包括新的新鲜令牌并设置retry
标志,这样,如果API反复出现,我们就不会进入无限循环响应401错误。我们需要将resolve
和reject
参数传递给AXIOS,否则我们的新承诺将永远不会被解决/拒绝。
如果由于任何原因而无法刷新令牌-我们拒绝承诺。我们不能简单地引发错误,因为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));
});
其余的是应用程序特定代码: