记录--前端无感知刷新token & 超时自动退出
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助
前端无感知刷新token&超时自动退出
一、token的作用
因为http请求是无状态的,是一次性的,请求之间没有任何关系,服务端无法知道请求者的身份,所以需要鉴权,来验证当前用户是否有访问系统的权限。
以oauth2.0授权码模式为例:
每次请求资源服务器时都会在请求头中添加 Authorization: Bearer access_token 资源服务器会先判断token是否有效,如果无效或过期则响应 401 Unauthorize。此时用户处于操作状态,应该自动刷新token保证用户的行为正常进行。
刷新token:使用refresh_token获取新的access_token,使用新的access_token重新发起失败的请求。
二、无感知刷新token方案
2.1 刷新方案
当请求出现状态码为 401 时表明token失效或过期,拦截响应,刷新token,使用新的token重新发起该请求。
如果刷新token的过程中,还有其他的请求,则应该将其他请求也保存下来,等token刷新完成,按顺序重新发起所有请求。
2.2 原生AJAX请求
2.2.1 http工厂函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | // 是否正在刷新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() // 按顺序重新发起所有失败的请求 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 } // 发起请求 httpRequest({ method: 'get' , url: 'http://127.0.0.1:8000/api/v1/getlist' }) |
2.3 Axios 无感知刷新token
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | // 是否正在刷新token的标记 let isRefreshing = false let requests: ReadonlyArray<(config: any) => void > = [] // 错误响应拦截 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放进队列,用一个函数形式来保存,等token刷新后直接执行 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 } }) |
三、长时间无操作超时自动退出
当用户登录之后,长时间不操作应该做自动退出功能,提高用户数据的安全性。
3.1 操作事件
操作事件:用户操作事件主要包含鼠标点击、移动、滚动事件和键盘事件等。
特殊事件:某些耗时的功能,比如上传、下载等。
3.2 方案
用户在登录页面之后,可以复制成多个标签,在某一个标签有操作,其他标签也不应该自动退出。所以需要标签页之间共享操作信息。这里我们使用 localStorage 来实现跨标签页共享数据。
在 localStorage 存入两个字段:
当有操作事件时,将当前时间戳存入 lastActiveTime。
当有特殊事件时,将特殊事件名称存入 activeEvents ,等特殊事件结束后,将该事件移除。
设置定时器,每1分钟获取一次 localStorage 这两个字段,优先判断 activeEvents 是否为空,若不为空则更新 lastActiveTime 为当前时间,若为空,则使用当前时间减去 lastActiveTime 得到的值与规定值(假设为1h)做比较,大于 1h 则退出登录。
3.3 代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | 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' )) } |
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· 【.NET】调用本地 Deepseek 模型
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 上周热点回顾(2.17-2.23)
2023-01-05 记录--微信小程序跳转H5、小程序、App
2021-01-05 file转化为binary对象发送给后台