Token用于進行接口鑒權吓著,但是Token具有由后端設置的過期時間偏化,當Token過期以后,就無法再請求數(shù)據(jù)了
項目中后端設置的過期時間為24h,測試時我們可以手動修改token值讓Token失效
處理方式:
- 方式1:用戶重新登錄,獲得新的Token就可以了贤笆,但是當過期時間較短的時候,每次都是要重新登錄操作 的,體驗很差
- 為了提高用戶的信息安全性捅暴,Token的過期時間都比較短(就算萬一泄露了,過一會兒也就過期無效化了)
- 方式2:根據(jù)用戶信息咧纠,自動給用戶生成新的Token蓬痒,減少登錄次數(shù)
我們觀察前面的功能的話,接口的響應信息中是有三個和token相關的信息的
- access_token:當前使用的token漆羔,用于訪問需要授權的接口
- expires_in:access_token的過期時間
- refresh_token:刷新獲取新的access_token
刷新Token 的方法有兩種:
方法一:
在每個請求發(fā)起前進行攔截梧奢,根據(jù)expires_in判斷token是否過期,如果過期則會刷新后再繼續(xù)請求接口- 優(yōu)點:請求前攔截處理演痒,能節(jié)省請求次數(shù)
- 缺點:后端需要提供Token過期時間字段(例如:expires_in)亲轨,并且需要結合計算機本地時間判斷,如果計算機時間被篡改(特別是比服務器時間滿)時鸟顺,攔截會失敗的
方法二:
在每個請求響應后進行攔截惦蚊,如果發(fā)現(xiàn)請求失敗(Token過期導致的)時讯嫂,刷新Token再刷新請求接口 - 優(yōu)點:無需Token過期時間字段养筒,無需判斷時間
- 缺點:多消耗一次請求
這里推薦使用方法二,相比較下來端姚,方法二更加的穩(wěn)定晕粪,不會出現(xiàn)意外的問題
Axios響應攔截器與錯誤處理
響應攔截器會在響應接收完畢,在對應請求處理前被攔截器攔截渐裸,響應攔截器參數(shù)response中保存了相應的信息
// Axios 官方文檔:響應攔截器
// Add a response interceptor
axios.interceptors.response.use(function (response) {
// Any status code that lie within the range of 2xx cause this function to trigger
// Do something with response data
return response;
}, function (error) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
return Promise.reject(error);
});
那么我們接來下將響應攔截器設置到utils/request.js中巫湘,將axios更改為創(chuàng)建的request(因為我們使用了ESLint規(guī)范装悲,記得去除所有的分號)
- error是需要console.dir()輸出的
// utils/request.js
...
// 設置響應攔截器
request.interceptors.response.use(function (response) {
// 狀態(tài)碼為 2xx 都會進入這里
console.log('請求響應成功了:', response)
return response
}, function (error) {
// 超出 2xx 都會進入這里
console.dir(error)
return Promise.reject(error)
})
export default request
Axios錯誤處理
錯誤處理,需要在攔截器中找到特定的錯誤情況進行token刷新
當出現(xiàn)錯誤時尚氛,通過Elemnt的Message組件設置提示诀诊,這里我們采用的是引入方式操作
- 引入的Message與之前使用的this.$message是相同的,只是引入方式與操作方式不同
// 通過局部引入的方式阅嘶,引入Element的Message組件功能
import { Message } from 'element-ui'
// 響應攔截器
request.interceptors.response.use(function (response) {
// 狀態(tài)碼2xx會執(zhí)行這里
console.log('響應成功了', response)
return response
}, function (error) {
if (error.response) {
// 請求發(fā)送成功属瓣,響應接收完畢,但是狀態(tài)碼為失敗的情況
// 1.判斷失敗的狀態(tài)碼情況(主要處理401的情況)
const { status } = error.response
let errorMessage = ''
if (status === 400) {
errorMessage = '請求參數(shù)錯誤'
} else if (status === 401) {
// 2.Token無效(過期)處理
errorMessage = 'Token 無效'
} else if (status === 403) {
errorMessage = '沒有權限讯柔,請聯(lián)系管理員'
} else if (status === 404) {
errorMessage = '請求資源不存在'
} else if (status >= 500) {
errorMessage = '服務器錯誤抡蛙,請聯(lián)系管理員'
}
Message.error(errorMessage)
} else if (error.request) {
// 請求發(fā)送成功,未收到響應
Message.error('請求超時請重試')
} else {
// 意料之外的錯誤
Message.error(error.message)
}
// 將本次請求的錯誤對象繼續(xù)向后拋出魂迄,讓接收響應的處理函數(shù)進行操作
return Promise.reject(error)
})
刷新Token
HTTP 狀態(tài)碼401表示未授權粗截,導致401的情況有:
- 沒有Token
- Token無效
- Token過期
判斷方法:- 檢測是否存在refresh_token:(后端通常會限制每個refresh_token只能獲取一次新的Token)
- 如果有,那就通過refresh_token獲取新的access_token
- 獲取成功捣炬,重啟發(fā)送請求熊昌,請求接口數(shù)據(jù)就行
- 獲取失敗,跳轉登錄頁
- 如果沒有湿酸,跳轉登錄頁
由于要進行跳轉婿屹,在utils/request.js中引入router/index.js
- 如果有,那就通過refresh_token獲取新的access_token
- 檢測是否存在refresh_token:(后端通常會限制每個refresh_token只能獲取一次新的Token)
// utils/request.js
// 引入 router
import router from '@/router'
首先要檢測store是否有user信息(有就證明是正常登陸,一定存在的有refresh_token)推溃,如果存在的有refresh_token的話就請求新的access_token昂利,需要用到對應的刷新接口,接下來檢查是否有新的access_token
- 失敗的話美莫,清除用戶信息页眯,跳轉登錄頁
- 跳轉登錄操作與之前是一致的,建議封裝起來
- 成功的話厢呵,更新access_token窝撵,同時重新請求之前401的接口
// utils/
...
// 封裝跳轉登錄頁面的函數(shù)
function redirectLogin () {
router.push({
name: 'login',
query: {
// router.currentRoute 用于獲取當前路由對應的路由信息對象
redirect: router.currentRoute.fullPath
}
})
}
// 設置響應攔截器
request.interceptors.response.use(function (response) {
...
}, function (error) {
// 超出 2xx 都會進入這里
if (error.response) {
...
} else if (status === 401) {
if (!store.state.user) {
/* router.push({
name: 'login',
query: {
// router.currentRoute 用于獲取當前路由對應的路由
redirect: router.currentRoute.fullPath
}
}) */
// 封裝函數(shù)后更改為調(diào)用
redirectLogin()
// 阻止后續(xù)操作,向下拋出錯誤對象
return Promise.reject(error)
}
...
}).then(res => {
if (res.data.state !== 1) {
// 清除已經(jīng)無效的用戶信息
store.commit('setUser', null)
// 跳轉登錄頁
/* router.push({
name: 'login',
query: {
// router.currentRoute 用于獲取當前路由對應的路由
redirect: router.currentRoute.fullPath
}
}) */
// 封裝函數(shù)后更改為調(diào)用
redirectLogin()
// 阻止后續(xù)操作襟铭,向下拋出錯誤對象
return Promise.reject(error)
}
...
}).catch(() => {
store.commit('setUser', null)
/* router.push({
name: 'login',
query: {
// router.currentRoute 用于獲取當前路由對應的路由
redirect: router.currentRoute.fullPath
}
}) */
// 封裝函數(shù)后更改為調(diào)用
redirectLogin()
return Promise.reject(error)
})
} else if (status === 403) {
...
處理Token重復刷新
如果頁面中存在多個請求(大多數(shù)頁面中都不會只有一次請求)碌奉,如果Token過期,每個請求都會刷新Token寒砖,這個時候刷新多次都沒有意義赐劣,又增加了請求個數(shù),還會出現(xiàn)額外的問題
通過瀏覽器的開發(fā)者工具觀察魁兼,有兩次的刷新Token請求,由于兩次的刷新token攜帶的refresh_token相同漠嵌,會導致一次成功一次失敗咐汞,失敗的那一次會導致頁面跳轉請求頁
為了避免多次請求刷新Token盖呼,可以通過一個變量isRefreshing標記Token的刷新狀態(tài)
- 默認狀態(tài)為false,并且在發(fā)送刷新Token請求前檢測化撕,狀態(tài)是false才能發(fā)送
- 發(fā)送刷新請求的時候几晤,設置標記為true
- 請求完畢,設置為false
// layout/components/app-header.vue
...
// 是否正在更新 Token
let isRefreshing = false
request.interceptors.response.use(function (response) {
...
} else if (status === 401) {
if (!store.state.user) {...}
// 發(fā)送刷新請求前判斷 isRefreshing 是否存在其他已發(fā)送的刷新請求
// 1 如果有植阴,則將當前請求掛起蟹瘾,等到 Token 刷新完畢再重發(fā),這里先設置為 return
if (isRefreshing) {
return
}
// 2. 如果沒有掠手,則更新 isRefreshing 并發(fā)送請求憾朴,繼續(xù)執(zhí)行后續(xù)操作
isRefreshing = true
// 發(fā)送刷新請求
return request({
...
}).then(res => {
...
}).catch(() => {
...
}).finally(() => {
// 3 請求完畢,無論成功失敗惨撇,設置 isRefreshing 為 false
isRefreshing = false
})
} else if (status === 403) {
...
雖然刷新Token的問題解決了伊脓,但是之前發(fā)送的兩個請求只有一個成功執(zhí)行府寒,其他的請求都被阻止了
如何解決魁衙?
我們聲明一個數(shù)組存儲所有被掛起的請求,當Token刷新完畢再將這些請求重新發(fā)送
// 存儲是否正在更新token 的狀態(tài)
let isRefreshing = false
// 存儲因為token刷新而掛起的請求
let requests = []
// 響應攔截器
request.interceptors.response.use(function (response) {
// 狀態(tài)碼2xx會執(zhí)行這里
console.log('響應成功了', response)
return response
}, function (error) {
if (error.response) {
// 請求發(fā)送成功株搔,響應接收完畢剖淀,但是狀態(tài)碼為失敗的情況
// 1.判斷失敗的狀態(tài)碼情況(主要處理401的情況)
const { status } = error.response
let errorMessage = ''
if (status === 400) {
errorMessage = '請求參數(shù)錯誤'
} else if (status === 401) {
// 2.Token無效(過期)處理
// 第一,無token信息
if (!store.state.user) {
redirectLogin()
return Promise.reject(error)
}
// 檢測是否已經(jīng)存在了正在刷新token的請求
if (isRefreshing) {
// 將當前失敗的請求存起來纤房,存儲到請求列表中
return requests.push(() => {
// 當前函數(shù)調(diào)用后纵隔,會自動發(fā)送本次失敗請求
request(error.config)
})
}
isRefreshing = true
// 第二,Token無效(錯誤Token炮姨,過期Token)
// 發(fā)送請求捌刮,獲取新的access_token
return request({
method: 'POST',
url: '/front/user/refresh_token',
data: qs.stringify({
refreshtoken: store.state.user.refresh_token
})
}).then(res => {
// -刷新token失敗
if (res.data.state !== 1) {
// 清除無效的用戶信息
store.commit('setUser', null)
// 封裝重復的跳轉登錄操作
redirectLogin()
return Promise.reject(error)
}
// 刷新token成功
// 存儲新的token
store.commit('setUser', res.data.content)
// 重新發(fā)送失敗的請求
// 根據(jù)reques
// 發(fā)送多次失敗的請求
requests.forEach(callback => callback())
// 發(fā)送完畢清除requests 內(nèi)容即可
requests = []
// 將本次請求發(fā)送
return request(error.config)
}).catch(err => {
console.log(err)
}).finally(() => {
// 無論成功還是失敗都會執(zhí)行
// 請求發(fā)送完畢,響應處理完畢舒岸,刷新狀態(tài)更改為false就行了
isRefreshing = false
})
解決