一、需求描述:
前端登錄後,後端返回acToken和acToken有效時間以及refreshToken,然後在request.headers帶上acToken,當acToken過期時要用refreshToken去獲取新的acToken,當refreshToken過期前端跳轉到登錄頁,獲取新的acToken時要做到用戶無感知。
二、分析
當用戶發起一個請求時,判斷acToken是否已過期,若已過期則先調refreshToken
接口,拿到新的acToken後再繼續執行之前的請求。
這個問題的難點在於:
1、當同時發起多個請求,而刷新acToken的接口還沒返回,此時其他請求該如何處理?
2、假如請求中帶有用戶行爲,比如一個購買按鈕,點擊它會先發起用戶是否被禁用接口,如果沒有禁用,則跳轉到購買頁,否則彈窗提示。
三、方案
1、在請求發起前攔截每個請求,判斷acToken的有效時間是否已經過期,若已過期,則將請求掛起,先刷新acToken後再繼續請求。
- 優點: 在請求前攔截,能節省請求,省流量。
- 缺點: 需要後端額外提供一個acToken過期時間的字段;使用了本地時間判斷,若本地時間被篡改,特別是本地時間比服務器時間慢時,攔截會失敗。
PS:acToken有效時間建議是時間段,類似緩存的MaxAge,而不要是絕對時間。當服務器和本地時間不一致時,絕對時間會有問題。
2、不在請求前攔截,而是攔截返回後的數據。先發起請求,接口返回過期後,先刷新acToken,再進行一次重試。
- 優點:不需額外的acToken過期字段,不需判斷時間。
- 缺點: 1、會消耗多一次請求,耗流量 2、請求then之後有用戶行爲,無法執行。
因爲方案2沒有解決到難點2,故此使用方案1,如有辦法再研究
四、實現代碼如下:
let isRefreshing = false
let requests =[]
function getJwtTokenVO(){
return localStorage.getItem('jwtTokenVO')?JSON.parse(localStorage.getItem('jwtTokenVO')):''
}
// request interceptor
service.interceptors.request.use(
config => {
// 過濾掉刷新token接口(避免死循環!!!!)和登錄接口
if (config.url.indexOf('/refreshToken') >= 0 || config.url.indexOf('/login') >= 0) {
return config
}
const jwtTokenVO = getJwtTokenVO()
if (jwtTokenVO) {
config.headers={...config.headers,acToken:jwtTokenVO.accessToken}
const now = new Date().getTime()
const accessTokenExpire = jwtTokenVO.accessTokenExpire //這個是絕對時間戳
// 穩妥要提前一點時間刷新token
if (now > (accessTokenExpire-5000)) {
// 立即刷新token
if (!isRefreshing) {
console.log('正在刷新token')
isRefreshing = true
fetchRefreshToken({ token: jwtTokenVO.refreshToken }).then(resData => {
// 刷新token接口的時候可能引起refreshToken過期跳轉登錄頁,這裏沒有進service.interceptors.response
if (resData.code * 1 === 5004) {
localStorage.removeItem('jwtTokenVO')
let href = window.location.href
/* refreshToken失效 這個code需要再修改 */
window.location.href = `${location.origin}${location.pathname}${location.search}#/login?orgId=${localStorage.getItem('lc_orgId')}&next=${href}`
return ''
}
if (resData.code * 1 === 1) {
console.log('重新獲取accessToken 成功')
/* 重新賦值accessToken */
localStorage.setItem('jwtTokenVO', JSON.stringify(resData.data))
axios.defaults.headers['acToken'] = resData.data.accessToken
}
isRefreshing = false
return resData.data.accessToken
}).then((accessToken) => {
console.log('刷新token成功,執行請求隊列')
if(accessToken){
requests.forEach(cb => cb(accessToken))
// 執行完成後,清空隊列
requests = []
}
}).catch(res => {
console.error('刷新token error: ', res)
})
}
const requestList = new Promise((resolve) => {
requests.push((accessToken) => {
// 刷新config的舊token
config.headers['acToken'] = accessToken
resolve(config)
})
})
return requestList
//
}
}
return config
},
error => {
console.log(error) // for debug
return Promise.reject(error)
}
)
// 請求返回後攔截
instance.interceptors.response.use(response => {
const { code } = response.data
if (code === 5004) {
localStorage.removeItem('jwtTokenVO')
let href = window.location.href
/* refreshToken失效 這個code需要再修改 */
window.location.href = `${location.origin}${location.pathname}${location.search}#/login?orgId=${localStorage.getItem('lc_orgId')}&next=${href}`
}
return response
}, error => {
console.log('catch', error)
return Promise.reject(error)
})