前言
目前登錄有很多用的是token機(jī)制, 因?yàn)榘踩詥?wèn)題, 一般都會(huì)返回一個(gè)刷新token 和 使用的token, 還有過(guò)期時(shí)間, 可以根據(jù)過(guò)期時(shí)間, 當(dāng)現(xiàn)有token失效時(shí), 重新獲取新的token

token機(jī)制.png
當(dāng)token失效或即將失效時(shí), 重新獲取新的token, 但因?yàn)閍jax是異步的, 請(qǐng)求新的token是需要一定時(shí)間, 此時(shí)若是有新的請(qǐng)求接口, 就會(huì)出現(xiàn)問(wèn)題
在網(wǎng)上查詢資料后, 一般有兩種方法可以實(shí)現(xiàn)無(wú)痛刷新token機(jī)制:
- 在請(qǐng)求攔截中, 攔截刷新token時(shí), 將額外接口緩存, 刷新成功后在重新發(fā)送請(qǐng)求
- 在響應(yīng)攔截中, 將額外接口緩存, 刷新成功后在重新發(fā)送請(qǐng)求
因?yàn)橐婚_(kāi)始找到的方法就是第二種, 所以直接使用第二種方法
此方法有一個(gè)缺點(diǎn): 就是緩存接口會(huì)請(qǐng)求二次, 消耗性能
解決思路
基于axios的vue框架
大致思路:
- 判斷token是否過(guò)期 以及 是否即將過(guò)期
- 過(guò)期后, 發(fā)送刷新token請(qǐng)求, 并將刷新token期間請(qǐng)求的接口緩存起來(lái)
- 請(qǐng)求成功后, 在重新請(qǐng)求之前的請(qǐng)求
方案
// 創(chuàng)建axios實(shí)例
const service = axios.create({
withCredentials: false, // 請(qǐng)求不帶cookie
baseURL: baseURL, // api 的 base_url
timeout: timeout // 請(qǐng)求超時(shí)時(shí)間
});
// 請(qǐng)求攔截器沒(méi)有做處理
service.interceptors.request.use(config => {
...
return config
})
// 是否正在刷新的標(biāo)記 -- 防止重復(fù)發(fā)出刷新token接口
let isRefreshing = false;
// 判斷token是否失效: return: true為過(guò)期
function isOAverdue() {
// 當(dāng)離過(guò)期時(shí)間還有半小時(shí)時(shí), 也判斷為過(guò)期
// getTokenItem('time'): 獲取存入localStorage的過(guò)期時(shí)間
return Math.floor((Date.now() - getTokenItme('time')) / 1000) + 30 * 60 >
getTokenItme('expires_in');
}
// 失效后同時(shí)發(fā)送請(qǐng)求的容器 -- 緩存接口
let subscribers = [];
// 刷新 token 后, 將緩存的接口重新請(qǐng)求一次
function onAccessTokenFetched(newToken) {
subscribers.forEach((callback) => {
callback(newToken);
});
// 清空緩存接口
subscribers = [];
}
// 添加緩存接口
function addSubscriber(callback) {
subscribers.push(callback);
}
// 響應(yīng)攔截器
service.interceptors.response.use(
response => {
// 當(dāng)response.data.re為401, 則判斷token已經(jīng)過(guò)期
// /oauth/token為刷新token的接口, 需要排除掉
if ((isOAverdue() || response.data.ret === 401) && !response.config.url.includes('/oauth/token')) {
if (!isRefreshing) {
isRefreshing = true;
// 將刷新token的方法放在vuex中處理了, 可見(jiàn)下面區(qū)塊代碼
store.dispatch('refreshToken').then((res) => {
// 當(dāng)刷新成功后, 重新發(fā)送緩存請(qǐng)求
onAccessTokenFetched(res);
}).catch(() => {
// 刷新token報(bào)錯(cuò)的話, 就需要跳轉(zhuǎn)到登錄頁(yè)面
window.location = '/#/guide/login';
}).finally(() => {
isRefreshing = false;
});
}
// 將其他接口緩存起來(lái) -- 這個(gè)Promise函數(shù)很關(guān)鍵
const retryOriginalRequest = new Promise((resolve) => {
// 這里是將其他接口緩存起來(lái)的關(guān)鍵, 返回Promise并且讓其狀態(tài)一直為等待狀態(tài),
// 只有當(dāng)token刷新成功后, 就會(huì)調(diào)用通過(guò)addSubscriber函數(shù)添加的緩存接口,
// 此時(shí), Promise的狀態(tài)就會(huì)變成resolve
addSubscriber((newToken) => {
// 表示用新的token去替換掉原來(lái)的token
response.config.headers.Authorization = `bearer ${newToken}`;
// 替換掉url -- 因?yàn)閎aseURL會(huì)擴(kuò)展請(qǐng)求url
response.config.url = response.config.url.replace(response.config.baseURL, '');
// 用重新封裝的config去請(qǐng)求, 就會(huì)將重新請(qǐng)求后的返回
resolve(service(response.config));
});
});
return retryOriginalRequest;
}
// ========================
// .... 省略其他代碼.....
}
// vuex中刷新token方法
async refreshToken({ commit }) {
// getTokenItem('refresh_token): 獲取刷新token
try {
let res = await refreshToken(getTokenItem('refresh_token));
const data = res.access_token;
newSetToken(res);
return data;
} catch(e) {
return '刷新token出錯(cuò)';
}
}