Token無縫刷新
前后端分離的系統,假如這個系統是給客服人員使用,客服人員可能要在長達八個小時的時間上不停地工作;為了安全考慮,不給token設定永久的時常,而是給它一個可刷新時間區域:在當前token剛剛過期后的一段時間發起的請求,系統可以無縫刷新一次token,從而為使用者帶來更好的體驗,這是非常重要的。
Axios中的實現
原理簡單闡述
實現很簡單,在攔截器當中檢測返回數據,檢測到剛剛過期可以刷新的情況,則在攔截器中直接發起刷新token的請求,獲得到新token后保存,再執行一次失敗的請求,執行完畢返回。
會遇到的問題
axios是異步執行的,假如在刷新token的過程中又有其他攜帶舊token並收到需要刷新的標識后,又如何處理呢?
設置一個全局標識,表明當前是否正在刷新token,如果正在刷新,就拒絕。這樣顯然是不好的。在一次需求中可能要發起多個請求這是很正常的事情。為了不讓其他請求失敗,我的做法是,創建一個異步任務順序執行器,在刷新token期間還攜帶舊token並返回過期的請求,則重新將本請求加入到這個執行器的末尾;而執行器則是在首次檢測到過期時進行初始化,並開始執行第一個任務,也就是刷新token任務。
代碼
放代碼之前我先給出一些解釋:我並沒有使用常規的http狀態碼來判斷響應結果,而是在后端程序中講所有非系統錯誤的大部分異常全部封裝為了自定義的錯誤碼並返回給前台,我在攔截器中自己來判斷返回結果。
/* eslint-disable no-unused-vars */
'use strict'
import Vue from 'vue'
import axios from 'axios'
import router from '../router.js'
import { Notification, Message } from 'element-ui'
// Full config: https://github.com/axios/axios#request-config
// axios.defaults.baseURL = process.env.baseURL || process.env.apiUrl || ''
// axios.defaults.headers.common['Authorization'] = AUTH_TOKEN
// axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'
let config = {
// baseURL: process.env.baseURL || process.env.apiUrl || ''
// timeout: 60 * 1000, // Timeout
// withCredentials: true, // Check cross-site Access-Control,
}
const _axios = axios.create(config)
_axios.interceptors.request.use(
function (config) {
// Do something before request is sent
if (!config.url.includes('http')) {
config.url = 'http://127.0.0.1:4399' + config.url
}
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = token
}
return config
},
function (error) {
// Do something with request error
return Promise.reject(error)
}
)
let refreshTokenFlag = false
let requestList = []
function executeRequests(index) {
if (index < requestList.length) {
requestList[index]().then(() => {
executeRequests(index + 1)
})
} else {
refreshTokenFlag = false
requestList.splice(0, requestList.length)
}
}
// Add a response interceptor
_axios.interceptors.response.use(
function (response) {
console.log(` >>> 攔截器開始`)
console.log(response.config.url)
console.log(response.config.headers.Authorization)
console.log(response.data)
// Do something with response data
/**
* 在這里返回Promise.reject會進入代碼的catch塊;
* 返回Promise.resolve或正常數據會進入then塊;
* 不返回 = 返回undefined也會進入then塊
*/
// 成功
if (response.data.code === 1) {
return response.data
}
if (response.data.code === 2 || response.data.code === 4) {
// 失敗 || 無權訪問
Message({
message: response.data.data || '網絡繁忙!',
type: 'error',
center: true
})
return Promise.reject(response.data)
} else if (response.data.code === 3) {
// 需要登錄驗證權限
const redirectUrl = router.currentRoute.fullPath
router.push({
path: '/login',
query: {
redirectUrl
}
})
return Promise.reject(response.data)
} else if (response.data.code === 5) {
// 需要刷新token
const userId = response.data.data
if (refreshTokenFlag) {
// 正在刷新
return new Promise((resolve, reject) => {
requestList.push(async function () {
try {
const r = await _axios(response.config)
resolve(r)
} catch (err) {
reject(err)
}
})
})
} else {
// 未刷新
refreshTokenFlag = true
return new Promise((resolve, reject) => {
requestList.push(async function () {
try {
const result = await _axios.post(`/sign/refreshToken/${userId}`)
localStorage.setItem('token', result.data)
const r = await _axios(response.config)
resolve(r)
} catch (err) {
reject(err)
}
})
executeRequests(0)
})
}
}
},
function (error) {
console.log('Ajax系統錯誤')
Notification.error({
title: '系統錯誤',
message: `${error || 'No message available'}`
})
return Promise.reject(error)
}
)
Plugin.install = function (Vue, options) {
Vue.axios = _axios
window.axios = _axios
Object.defineProperties(Vue.prototype, {
axios: {
get() {
return _axios
}
},
$axios: {
get() {
return _axios
}
},
})
}
Vue.use(Plugin)
export default Plugin
其他
其實無痛刷新token的方法有很多,我在之前用到的方法則是在springmvc的Interceptor以及ResponseBodyAdvice中進行token的刷新,在返回結果中攜帶一個token刷新標志以及刷新后的token;這帶來了一個問題:所有的token刷新都會在后端來解決,對於一個小系統,小網站來說,很可能會影響程序執行效率(當然大系統應該有自己的一套方案,這我就不知道了,若是有大佬路過也很期待您能在評論中給出一個小建議,感激不盡)。
在實現通過前端來刷新的這個方法后,經過測試發現,體驗還不錯,在刷新的過程會有比較低的延遲但完全可以接受(延遲若是很大的話可能本身列入隊列的請求也很復雜),所以寫下這篇文章以便記錄。