需求
當(dang)前前后端普遍使用(yong)token進行鑒權,當(dang)token過期(qi)后,用(yong)戶(hu)需要重(zhong)新登錄,輸入用(yong)戶(hu)名和(he)密碼以獲取新的(de)token。一般token過期(qi)時間設置很短,對于(yu)比(bi)較活躍的(de)用(yong)戶(hu)體驗感非常差。那么如(ru)何(he)解(jie)決(jue)這個問題呢?
這里引(yin)入兩個新(xin)的名(ming)詞access_token和refresh_token。access_token為授(shou)權令牌,用(yong)(yong)于驗證用(yong)(yong)戶身份(fen),是在調用(yong)(yong)API時需要(yao)傳入的header請求參數。當access_token過期(qi)后,需要(yao)刷新(xin),但(dan)是每次(ci)刷新(xin)都需要(yao)填寫(xie)用(yong)(yong)戶名(ming)密碼,非常(chang)繁瑣。而refresh_token可以解決(jue)這個問題(ti),顧名(ming)思義,refresh_token為刷新(xin)token,用(yong)(yong)來刷新(xin)access_token,無需用(yong)(yong)戶進行附加操作。
- 當access_token過期(qi),前端拿著refresh_token去獲(huo)取新(xin)的access_token,再重新(xin)發起請求,此(ci)時需要做到(dao)用(yong)戶無(wu)感知。
- 當用戶(hu)同時(shi)發起多個(ge)請(qing)求時(shi),第(di)一(yi)個(ge)請(qing)求會去調(diao)(diao)用刷新token接(jie)(jie)口(kou),當該接(jie)(jie)口(kou)還沒返回(hui)時(shi),其(qi)他的請(qing)求也去調(diao)(diao)用了刷新token接(jie)(jie)口(kou),此時(shi)會產生多個(ge)請(qing)求,前端需要避免這種情況(kuang)出現。
方案
利用axios的響應攔截(jie)器對返回的數據做處理。不(bu)需(xu)要(yao)解析access_token和refresh_token拿(na)到各(ge)自的過期時間去做判(pan)斷,但是需(xu)要(yao)多發送一(yi)次http請求。
實現
- @/utils/cookies
import Cookies from 'js-cookie'
export default Cookies
// Token
const tokenKey = 'access_token'
export const getToken = (): string | undefined => Cookies.get(tokenKey)
export const setToken = (token: string): unknown => Cookies.set(tokenKey, token)
export const removeToken = (): unknown => Cookies.remove(tokenKey)
// refreshToken
const refreshTokenKey = 'refresh_token'
export const getRefreshToken = (): unknown => Cookies.get(refreshTokenKey)
export const setRefreshToken = (token: string): unknown => Cookies.set(refreshTokenKey, token)
export const removeRefreshToken = (): unknown => Cookies.remove(refreshTokenKey)
const usernameKey = 'username'
export const getUsername = (): unknown => Cookies.get(usernameKey)
export const setUsername = (token: string): unknown => Cookies.set(usernameKey, token)
export const removeUsername = (): unknown => Cookies.remove(usernameKey)
export const clearToken = (): void => {
removeToken()
removeRefreshToken()
removeUsername()
}
- @/utils/request.js
/** 創建axios實例 */
const service = axios.create({
baseURL: settings.apiBaseUrl,
timeout: 5 * 3600 * 1000,
})
/** 響應攔截器攔截器 */
service.interceptors.response.use(
(response: BaseAxiosResponse) => {
const { config, data } = response
if (data.code === 0) {
return Promise.resolve(response)
} else if (data.code === 400005) {
// token 過期處理邏輯
} else {
return Promise.reject(response?.data)
}
},
error => {
return Promise.reject(error)
}
)
響(xiang)應攔截器改造。當返回(hui)的code值為400005時,表示access_token無效。此時調用刷新接口重(zhong)新獲取access_token,并重(zhong)新發起原(yuan)請求。
/** 創建axios實例 */
const service = axios.create({
baseURL: settings.apiBaseUrl,
timeout: 5 * 3600 * 1000,
})
/** 響應攔截器攔截器 */
service.interceptors.response.use(
(response: BaseAxiosResponse) => {
const { config, data } = response
if (data.code === 0) {
return Promise.resolve(response)
} else if (data.code === 400005) {
// token 過期處理邏輯
const token = getRefreshToken()
return refreshToken({ oldRefreshToken: token })
.then(res => {
const { accessToken, refreshToken } = res.data
setToken(accessToken)
setRefreshToken(refreshToken)
config.headers.Authorization = accessToken
return service(config)
})
.catch(err => {
console.log('抱歉,您的登錄狀態已失效,請重新登錄!')
return Promise.reject(err)
})
} else {
// refreshToken 失效
if (config.url.includes(REFRESH_URL)) {
clearToken()
router.push('/login').catch(err => {
console.log(err)
})
}
return Promise.reject(response?.data)
}
},
error => {
return Promise.reject(error)
}
)
當用戶同時(shi)發起多個(ge)請(qing)求時(shi),可能存在多次調用刷(shua)新(xin)token的接(jie)口,因(yin)此需要定(ding)義一個(ge)標記(ji)來判斷當前(qian)是否處于刷(shua)新(xin)狀態,如果處于刷(shua)新(xin)狀態,則禁止其(qi)他請(qing)求調用刷(shua)新(xin)接(jie)口。
/** 創建axios實例 */
const service = axios.create({
baseURL: settings.apiBaseUrl,
timeout: 5 * 3600 * 1000,
})
let isRefreshing = false // 標記是否正在刷新 token
/** 響應攔截器攔截器 */
service.interceptors.response.use(
(response: BaseAxiosResponse) => {
const { config, data } = response
if (data.code === 0) {
return Promise.resolve(response)
} else if (data.code === 400005) {
// token 過期處理邏輯
if (!isRefreshing) {
isRefreshing = true
const token = getRefreshToken()
return refreshToken({ oldRefreshToken: token })
.then(res => {
const { accessToken, refreshToken } = res.data
setToken(accessToken)
setRefreshToken(refreshToken)
config.headers.Authorization = accessToken
return service(config)
})
.catch(err => {
router.push('/login').catch(err => {
console.log(err)
})
return Promise.reject(err)
})
.finally(() => {
isRefreshing = false
})
} else {
// refreshToken 失效
if (config.url.includes(REFRESH_URL)) {
clearToken()
router.push('/login').catch(err => {
console.log(err)
})
}
return Promise.reject(response?.data)
}
},
error => {
return Promise.reject(error)
}
)
上(shang)述(shu)同(tong)時發起多個請求(qiu)的(de)方法可以進一(yi)步(bu)優化(hua)。
當(dang)發(fa)起(qi)多個(ge)請(qing)求,第一個(ge)請(qing)求進入刷新(xin)token的流(liu)程,需(xu)要將(jiang)其他請(qing)求掛起(qi),當(dang)token更新(xin)之(zhi)后(hou)在重新(xin)發(fa)起(qi)請(qing)求。這(zhe)里定義一個(ge)requests數組(zu),暫存掛起(qi)的請(qing)求。
/** 創建axios實例 */
const service = axios.create({
baseURL: settings.apiBaseUrl,
timeout: 5 * 3600 * 1000,
})
let isRefreshing = false // 標記是否正在刷新 token
const requests = [] // 存儲待重發請求的數組
/** 響應攔截器攔截器 */
service.interceptors.response.use(
(response: BaseAxiosResponse) => {
const { config, data } = response
if (data.code === 0) {
return Promise.resolve(response)
} else if (data.code === 400005) {
// token 過期處理邏輯
if (!isRefreshing) {
isRefreshing = true
const token = getRefreshToken()
return refreshToken({ oldRefreshToken: token })
.then(res => {
const { accessToken, refreshToken } = res.data
setToken(accessToken)
setRefreshToken(refreshToken)
config.headers.Authorization = accessToken
// token 刷新后將數組的方法重新執行
requests.forEach(cb => cb(accessToken))
return service(config)
})
.catch(err => {
router.push('/login').catch(err => {
console.log(err)
})
return Promise.reject(err)
})
.finally(() => {
isRefreshing = false
})
}else {
// 返回未執行 resolve 的 Promise
return new Promise(resolve => {
// 用函數形式將 resolve 存入,刷新token之后回調執行
requests.push(token => {
config.headers.Authorization = token
resolve(service(config))
})
})
}
} else {
// refreshToken 失效
if (config.url.includes(REFRESH_URL)) {
clearToken()
router.push('/login').catch(err => {
console.log(err)
})
}
return Promise.reject(response?.data)
}
},
error => {
return Promise.reject(error)
}
)