前端無感知刷新token以及超時(shí)自動(dòng)退出實(shí)現(xiàn)方案
一、token的作用
因?yàn)閔ttp請(qǐng)求是無狀態(tài)的,是一次性的,請(qǐng)求之間沒有任何關(guān)系,服務(wù)端無法知道請(qǐng)求者的身份,所以需要鑒權(quán),來驗(yàn)證當(dāng)前用戶是否有訪問系統(tǒng)的權(quán)限。
以oauth2.0授權(quán)碼模式為例:
每次請(qǐng)求資源服務(wù)器時(shí)都會(huì)在請(qǐng)求頭中添加 Authorization: Bearer access_token 資源服務(wù)器會(huì)先判斷token是否有效,如果無效或過期則響應(yīng) 401 Unauthorize。此時(shí)用戶處于操作狀態(tài),應(yīng)該自動(dòng)刷新token保證用戶的行為正常進(jìn)行。
刷新token:使用refresh_token獲取新的access_token,使用新的access_token重新發(fā)起失敗的請(qǐng)求。
二、無感知刷新token方案
2.1 刷新方案
當(dāng)請(qǐng)求出現(xiàn)狀態(tài)碼為 401 時(shí)表明token失效或過期,攔截響應(yīng),刷新token,使用新的token重新發(fā)起該請(qǐng)求。
如果刷新token的過程中,還有其他的請(qǐng)求,則應(yīng)該將其他請(qǐng)求也保存下來,等token刷新完成,按順序重新發(fā)起所有請(qǐng)求。
2.2 原生AJAX請(qǐng)求
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請(qǐng)求中斷 HTTPPromise.abort = () => xhr.abort() ? return HTTPPromise; }
2.2.2 無感知刷新token
// 是否正在刷新token的標(biāo)記 let isRefreshing = false ? // 存放因token過期而失敗的請(qǐng)求 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ā)起所有失敗的請(qǐng)求 const allRequests = [() => resolve(httpRequest(config)), ...requests] allRequests.forEach((cb) => cb()) } else { // 正在刷新token,將請(qǐng)求暫存 requests = [ ...requests, () => resolve(httpRequest(config)), ] } } catch(err) { reject(err) } finally { isRefreshing = false requests = [] } } } catch(ex) { reject(ex) } }) process.abort = abort return process } ? // 發(fā)起請(qǐng)求 httpRequest({ method: 'get', url: 'http://127.0.0.1:8000/api/v1/getlist' })
2.3 Axios 無感知刷新token
// 是否正在刷新token的標(biāo)記 let isRefreshing = false ? let requests: ReadonlyArray<(config: any) => void> = [] ? // 錯(cuò)誤響應(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放進(jìn)隊(duì)列,用一個(gè)函數(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 } })
三、長(zhǎng)時(shí)間無操作超時(shí)自動(dòng)退出
當(dāng)用戶登錄之后,長(zhǎng)時(shí)間不操作應(yīng)該做自動(dòng)退出功能,提高用戶數(shù)據(jù)的安全性。
3.1 操作事件
操作事件:用戶操作事件主要包含鼠標(biāo)點(diǎn)擊、移動(dòng)、滾動(dòng)事件和鍵盤事件等。
特殊事件:某些耗時(shí)的功能,比如上傳、下載等。
3.2 方案
用戶在登錄頁面之后,可以復(fù)制成多個(gè)標(biāo)簽,在某一個(gè)標(biāo)簽有操作,其他標(biāo)簽也不應(yīng)該自動(dòng)退出。所以需要標(biāo)簽頁之間共享操作信息。這里我們使用 localStorage 來實(shí)現(xiàn)跨標(biāo)簽頁共享數(shù)據(jù)。
在 localStorage 存入兩個(gè)字段:
名稱 | 類型說明 | 說明 |
---|---|---|
lastActiveTime | string | 最后一次觸發(fā)操作事件的時(shí)間戳 |
activeEvents | string[ ] | 特殊事件名稱數(shù)組 |
當(dāng)有操作事件時(shí),將當(dāng)前時(shí)間戳存入 lastActiveTime。
當(dāng)有特殊事件時(shí),將特殊事件名稱存入 activeEvents ,等特殊事件結(jié)束后,將該事件移除。
設(shè)置定時(shí)器,每1分鐘獲取一次 localStorage 這兩個(gè)字段,優(yōu)先判斷 activeEvents 是否為空,若不為空則更新 lastActiveTime 為當(dāng)前時(shí)間,若為空,則使用當(dāng)前時(shí)間減去 lastActiveTime 得到的值與規(guī)定值(假設(shè)為1h)做比較,大于 1h 則退出登錄。
3.3 代碼實(shí)現(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) ? /** * 頁面超時(shí)未有操作事件退出登錄 */ export function timeout(keepTime = 60) { document.addEventListener('mousedown', updateActivityStatus) document.addEventListener('mouseover', updateActivityStatus) document.addEventListener('wheel', updateActivityStatus) document.addEventListener('keydown', updateActivityStatus) ? // 定時(shí)器 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() } ? /** * 重置定時(shí)器 */ 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以及超時(shí)自動(dòng)退出實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)前端無感知刷新token內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
現(xiàn)代 JavaScript 開發(fā)編程風(fēng)格Idiomatic.js指南中文版
下面的章節(jié)描述的是一個(gè) 合理 的現(xiàn)代 JavaScript 開發(fā)風(fēng)格指南,并非硬性規(guī)定。其想送出的核心理念是高度統(tǒng)一的代碼風(fēng)格(the law of code style consistency)。2014-05-05js實(shí)現(xiàn)彈窗居中的簡(jiǎn)單實(shí)例
下面小編就為大家?guī)硪黄猨s實(shí)現(xiàn)彈窗居中的簡(jiǎn)單實(shí)例。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-10-10javascript checkbox/radio onchange不能兼容ie8處理辦法
這篇文章主要介紹了javascript checkbox/radio onchange不能兼容ie8處理辦法的相關(guān)資料,需要的朋友可以參考下2017-06-06JavaScript檢查數(shù)字是否為整數(shù)或浮點(diǎn)數(shù)的方法
這篇文章主要介紹了JavaScript檢查數(shù)字是否為整數(shù)或浮點(diǎn)數(shù)的方法,涉及javascript類型判斷的相關(guān)技巧,需要的朋友可以參考下2015-06-06微信公眾號(hào)平臺(tái)接口開發(fā) 獲取access_token過程解析
這篇文章主要介紹了微信公眾號(hào)平臺(tái)接口開發(fā) 獲取access_token過程解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-08-08JS獲取數(shù)組最大值、最小值及長(zhǎng)度的方法
這篇文章主要介紹了JS獲取數(shù)組最大值、最小值及長(zhǎng)度的方法,涉及JavaScript遍歷數(shù)組及l(fā)ength屬性的相關(guān)使用技巧,非常簡(jiǎn)潔實(shí)用,需要的朋友可以參考下2015-11-11javascript+Canvas實(shí)現(xiàn)畫板功能
這篇文章主要為大家詳細(xì)介紹了javascript+Canvas實(shí)現(xiàn)畫板功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-06-06