前端無感知刷新token以及超時自動退出實現(xiàn)方案
一、token的作用
因為http請求是無狀態(tài)的,是一次性的,請求之間沒有任何關(guān)系,服務(wù)端無法知道請求者的身份,所以需要鑒權(quán),來驗證當前用戶是否有訪問系統(tǒng)的權(quán)限。
以oauth2.0授權(quán)碼模式為例:

每次請求資源服務(wù)器時都會在請求頭中添加 Authorization: Bearer access_token 資源服務(wù)器會先判斷token是否有效,如果無效或過期則響應(yīng) 401 Unauthorize。此時用戶處于操作狀態(tài),應(yīng)該自動刷新token保證用戶的行為正常進行。
刷新token:使用refresh_token獲取新的access_token,使用新的access_token重新發(fā)起失敗的請求。
二、無感知刷新token方案
2.1 刷新方案
當請求出現(xiàn)狀態(tài)碼為 401 時表明token失效或過期,攔截響應(yīng),刷新token,使用新的token重新發(fā)起該請求。
如果刷新token的過程中,還有其他的請求,則應(yīng)該將其他請求也保存下來,等token刷新完成,按順序重新發(fā)起所有請求。
2.2 原生AJAX請求
2.2.1 http工廠函數(shù)
function httpFactory({ method, url, body, headers, readAs, timeout }) {
const xhr = new XMLHttpRequest()
xhr.open(method, url)
xhr.timeout = isNumber(timeout) ? timeout : 1000 * 60
?
if(headers){
forEach(headers, (value, name) => value && xhr.setRequestHeader(name, value))
}
const HTTPPromise = new Promise((resolve, reject) => {
xhr.onload = function () {
let response;
?
if (readAs === 'json') {
try {
response = JSONbig.parse(this.responseText || null);
} catch {
response = this.responseText || null;
}
} else if (readAs === 'xml') {
response = this.responseXML
} else {
response = this.responseText
}
?
resolve({ status: xhr.status, response, getResponseHeader: (name) => xhr.getResponseHeader(name) })
}
?
xhr.onerror = function () {
reject(xhr)
}
xhr.ontimeout = function () {
reject({ ...xhr, isTimeout: true })
}
?
beforeSend(xhr)
?
body ? xhr.send(body) : xhr.send()
?
xhr.onreadystatechange = function () {
if (xhr.status === 502) {
reject(xhr)
}
}
})
?
// 允許HTTP請求中斷
HTTPPromise.abort = () => xhr.abort()
?
return HTTPPromise;
}
2.2.2 無感知刷新token
// 是否正在刷新token的標記
let isRefreshing = false
?
// 存放因token過期而失敗的請求
let requests = []
?
function httpRequest(config) {
let abort
let process = new Promise(async (resolve, reject) => {
const request = httpFactory({...config, headers: { Authorization: 'Bearer ' + cookie.load('access_token'), ...configs.headers }})
abort = request.abort
try {
const { status, response, getResponseHeader } = await request
?
if(status === 401) {
try {
if (!isRefreshing) {
isRefreshing = true
// 刷新token
await refreshToken()
?
// 按順序重新發(fā)起所有失敗的請求
const allRequests = [() => resolve(httpRequest(config)), ...requests]
allRequests.forEach((cb) => cb())
} else {
// 正在刷新token,將請求暫存
requests = [
...requests,
() => resolve(httpRequest(config)),
]
}
} catch(err) {
reject(err)
} finally {
isRefreshing = false
requests = []
}
}
} catch(ex) {
reject(ex)
}
})
process.abort = abort
return process
}
?
// 發(fā)起請求
httpRequest({ method: 'get', url: 'http://127.0.0.1:8000/api/v1/getlist' })
2.3 Axios 無感知刷新token
// 是否正在刷新token的標記
let isRefreshing = false
?
let requests: ReadonlyArray<(config: any) => void> = []
?
// 錯誤響應(yīng)攔截
axiosInstance.interceptors.response.use((res) => res, async (err) => {
if (err.response && err.response.status === 401) {
try {
if (!isRefreshing) {
isRefreshing = true
// 刷新token
const { access_token } = await refreshToken()
?
if (access_token) {
axiosInstance.defaults.headers.common.Authorization = `Bearer ${access_token}`;
?
requests.forEach((cb) => cb(access_token))
requests = []
?
return axiosInstance.request({
...err.config,
headers: {
...(err.config.headers || {}),
Authorization: `Bearer ${access_token}`,
},
})
}
?
throw err
}
?
return new Promise((resolve) => {
// 將resolve放進隊列,用一個函數(shù)形式來保存,等token刷新后直接執(zhí)行
requests = [
...requests,
(token) => resolve(axiosInstance.request({
...err.config,
headers: {
...(err.config.headers || {}),
Authorization: `Bearer ${token}`,
},
})),
]
})
} catch (e) {
isRefreshing = false
throw err
} finally {
if (!requests.length) {
isRefreshing = false
}
}
} else {
throw err
}
})
三、長時間無操作超時自動退出
當用戶登錄之后,長時間不操作應(yīng)該做自動退出功能,提高用戶數(shù)據(jù)的安全性。
3.1 操作事件
操作事件:用戶操作事件主要包含鼠標點擊、移動、滾動事件和鍵盤事件等。
特殊事件:某些耗時的功能,比如上傳、下載等。
3.2 方案
用戶在登錄頁面之后,可以復制成多個標簽,在某一個標簽有操作,其他標簽也不應(yīng)該自動退出。所以需要標簽頁之間共享操作信息。這里我們使用 localStorage 來實現(xiàn)跨標簽頁共享數(shù)據(jù)。
在 localStorage 存入兩個字段:
| 名稱 | 類型說明 | 說明 |
|---|---|---|
| lastActiveTime | string | 最后一次觸發(fā)操作事件的時間戳 |
| activeEvents | string[ ] | 特殊事件名稱數(shù)組 |
當有操作事件時,將當前時間戳存入 lastActiveTime。
當有特殊事件時,將特殊事件名稱存入 activeEvents ,等特殊事件結(jié)束后,將該事件移除。
設(shè)置定時器,每1分鐘獲取一次 localStorage 這兩個字段,優(yōu)先判斷 activeEvents 是否為空,若不為空則更新 lastActiveTime 為當前時間,若為空,則使用當前時間減去 lastActiveTime 得到的值與規(guī)定值(假設(shè)為1h)做比較,大于 1h 則退出登錄。
3.3 代碼實現(xiàn)
const LastTimeKey = 'lastActiveTime'
const activeEventsKey = 'activeEvents'
const debounceWaitTime = 2 * 1000
const IntervalTimeOut = 1 * 60 * 1000
?
export const updateActivityStatus = debounce(() => {
localStorage.set(LastTimeKey, new Date().getTime())
}, debounceWaitTime)
?
/**
* 頁面超時未有操作事件退出登錄
*/
export function timeout(keepTime = 60) {
document.addEventListener('mousedown', updateActivityStatus)
document.addEventListener('mouseover', updateActivityStatus)
document.addEventListener('wheel', updateActivityStatus)
document.addEventListener('keydown', updateActivityStatus)
?
// 定時器
let timer;
?
const doTimeout = () => {
timer && clearTimeout(timer)
localStorage.remove(LastTimeKey)
document.removeEventListener('mousedown', updateActivityStatus)
document.removeEventListener('mouseover', updateActivityStatus)
document.removeEventListener('wheel', updateActivityStatus)
document.removeEventListener('keydown', updateActivityStatus)
?
// 注銷token,清空session,回到登錄頁
logout()
}
?
/**
* 重置定時器
*/
function resetTimer() {
localStorage.set(LastTimeKey, new Date().getTime())
?
if (timer) {
clearInterval(timer)
}
?
timer = setInterval(() => {
const isSignin = document.cookie.includes('access_token')
if (!isSignin) {
doTimeout()
return
}
?
const activeEvents = localStorage.get(activeEventsKey)
if(!isEmpty(activeEvents)) {
localStorage.set(LastTimeKey, new Date().getTime())
return
}
const lastTime = Number(localStorage.get(LastTimeKey))
?
if (!lastTime || Number.isNaN(lastTime)) {
localStorage.set(LastTimeKey, new Date().getTime())
return
}
?
const now = new Date().getTime()
const time = now - lastTime
?
if (time >= keepTime) {
doTimeout()
}
}, IntervalTimeOut)
}
?
resetTimer()
}
?
// 上傳操作
function upload() {
const current = JSON.parse(localStorage.get(activeEventsKey))
localStorage.set(activeEventsKey, [...current, 'upload'])
...
// do upload request
...
const current = JSON.parse(localStorage.get(activeEventsKey))
localStorage.set(activeEventsKey, Array.isArray(current) ? current.filter((item) => itme !== 'upload'))
}總結(jié)
到此這篇關(guān)于前端無感知刷新token以及超時自動退出實現(xiàn)的文章就介紹到這了,更多相關(guān)前端無感知刷新token內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript統(tǒng)計數(shù)組中相同的數(shù)量的方法總結(jié)
在JavaScript中,我們經(jīng)常需要對數(shù)組中對象的屬性進行統(tǒng)計。在本文中,我們將介紹如何使用JavaScript來實現(xiàn)這一功能,文中有詳細的代碼示例,需要的朋友可以借鑒參考2023-05-05

