最近遇到個需求:前端登陸後,後端返回token和refresh_token,當token過時時用舊refresh_token去獲取新的token,前端須要作到無痛刷新token,即請求刷新token時要作到用戶無感知。前端
當用戶發起一個請求時,判斷token是否已過時,若已過時則先調refreshToken接口,拿到新的token後再繼續執行以前的請求。vue
這個問題的難點在於:當同時發起多個請求,而刷新token的接口還沒返回,此時其餘請求該如何處理?接下來會按部就班地分享一下整個過程。ios
這裏會使用axios來實現,以上方法是請求後攔截,因此會使用axios.interceptors.response.use()方法。vue-router
首先說明一下,項目中的token是存在localStorage中的。element-ui
如何防止屢次刷新token 若是refreshToken接口還沒返回,此時再有一個過時的請求進來,上面的代碼就會再一次執行refreshToken,這就會致使屢次執行刷新token的接口,所以須要防止這個問題。咱們能夠在request.js中用一個flag來標記當前是否正在刷新token的狀態,若是正在刷新則再也不調用刷新token的接口。axios
這樣子就能夠避免在刷新token時再進入方法了。可是這種作法是至關於把其餘失敗的接口給捨棄了,假如同時發起兩個請求,且幾乎同時返回,第一個請求確定是進入了refreshToken後再重試,而第二個請求則被丟棄了,還是返回失敗,因此接下來還得解決其餘接口的重試問題。後端
同時發起兩個或以上的請求時,其餘接口如何重試 兩個接口幾乎同時發起和返回,第一個接口會進入刷新token後重試的流程,而第二個接口須要先存起來,而後等刷新token後再重試。一樣,若是同時發起三個請求,此時須要緩存後兩個接口,等刷新token後再重試。因爲接口都是異步的,處理起來會有點麻煩。api
當第二個過時的請求進來,token正在刷新,咱們先將這個請求存到一個數組隊列中,想辦法讓這個請求處於等待中,一直等到刷新token後再逐個重試清空請求隊列。 那麼如何作到讓這個請求處於等待中呢?爲了解決這個問題,咱們得藉助Promise。將請求存進隊列中後,同時返回一個Promise,讓這個Promise一直處於Pending狀態(即不調用resolve),此時這個請求就會一直等啊等,只要咱們不執行resolve,這個請求就會一直在等待。當刷新請求的接口返回來後,咱們再調用resolve,逐個重試。最終代碼:跨域
import axios from 'axios'
import { Loading, Message, MessageBox } from 'element-ui'
import api from './api'
import { getToken, setToken, removeToken, getRefreshToken } from '../utils/cookies'
let UserModule = {
RefreshToken: (data) => {
setToken('Bearer ' + data.access_token, data.refresh_token)
}
}
// 是否正在刷新的標記
let isRefreshing = false
// 重試隊列,每一項將是一個待執行的函數形式
let retryRequests = []
const request = axios.create({
baseURL: api.baseUrl,
timeout: 50000,
withCredentials: true // cookie跨域必備
})
// http request 攔截器 Request
request.interceptors.request.use(
(config) => {
if (getToken()) {
config.headers['Authorization'] = getToken()
}
return config
},
(error) => {
Promise.reject(error)
}
)
// http response 攔截器 Response
request.interceptors.response.use(
(response) => {
// code == 0: 成功
const res = response.data
if (res.code !== 0) {
if (res.message) {
Message({
message: res.message,
type: 'error',
duration: 5 * 1000
})
}
return Promise.reject(res)
} else {
return response.data
}
},
(error) => {
if (!error.response) return Promise.reject(error)
// 根據refreshtoken從新獲取token
// 5000系統繁忙
// 5001參數錯誤
// 1003該用戶權限不足以訪問該資源接口
// 1004訪問此資源須要徹底的身份驗證
// 1001access_token無效
// 1002refresh_token無效
if (error.response.data.code === 1004 || error.response.data.code === 1001) {
const config = error.config
if (!isRefreshing) {
isRefreshing = true
return getRefreshTokenFunc()
.then((res) => {
// 從新設置token
UserModule.RefreshToken(res.data.data)
config.headers['Authorization'] = getToken()
// 已經刷新了token,將全部隊列中的請求進行重試
// @ts-ignore
retryRequests.forEach((cb) => cb(getToken()))
// 重試完清空這個隊列
retryRequests = []
// 這邊不須要baseURL是由於會從新請求url,url中已經包含baseURL的部分了
config.baseURL = ''
return request(config)
})
.catch(() => {
resetLogin()
})
.finally(() => {
isRefreshing = false
})
} else {
// 正在刷新token,返回一個未執行resolve的promise
return new Promise((resolve) => {
// 將resolve放進隊列,用一個函數形式來保存,等token刷新後直接執行
// @ts-ignore
retryRequests.push((token: any) => {
config.baseURL = ''
config.headers['Authorization'] = token
resolve(request(config))
})
})
}
} else if (error.response.data.code === 1002) {
resetLogin()
} else {
Message({
message: error.response.data.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
}
)
// 刷新token的請求方法
function getRefreshTokenFunc() {
let params = {
refresh_token: getRefreshToken() || ''
}
return axios.post(api.baseUrl + 'auth-center/auth/refresh_token', params)
}
function resetLogin(title = '身份驗證失敗,請從新登陸!') {
if (window.location.href.indexOf('/login') === -1) {
MessageBox.confirm(title, '退出', {
confirmButtonText: '從新登陸',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
removeToken()
location.reload() // To prevent bugs from vue-router
})
}
}
/**
* []請求
* @param params 參數
* @param operation 接口
*/
function customRequest(url: string, method: any, data: any) {
// service.defaults.headers['Content-Type']=contentType
let datatype = method.toLocaleLowerCase() === 'get' ? 'params' : 'data'
return request({
url: url,
method: method,
[datatype]: data
})
}
export { request, customRequest }
複製代碼