记录–前端无感知刷新token & 超时自动退出

  • 记录–前端无感知刷新token & 超时自动退出已关闭评论
  • 81 次浏览
  • A+
所属分类:Web前端
摘要

因为http请求是无状态的,是一次性的,请求之间没有任何关系,服务端无法知道请求者的身份,所以需要鉴权,来验证当前用户是否有访问系统的权限。


这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助

记录--前端无感知刷新token & 超时自动退出

前端无感知刷新token&超时自动退出

一、token的作用

因为http请求是无状态的,是一次性的,请求之间没有任何关系,服务端无法知道请求者的身份,所以需要鉴权,来验证当前用户是否有访问系统的权限。

以oauth2.0授权码模式为例:

记录--前端无感知刷新token & 超时自动退出

每次请求资源服务器时都会在请求头中添加 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工厂函数

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() ​                         // 按顺序重新发起所有失败的请求                         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

// 是否正在刷新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 存入两个字段:

记录--前端无感知刷新token & 超时自动退出

当有操作事件时,将当前时间戳存入 lastActiveTime。

当有特殊事件时,将特殊事件名称存入 activeEvents ,等特殊事件结束后,将该事件移除。

设置定时器,每1分钟获取一次 localStorage 这两个字段,优先判断 activeEvents 是否为空,若不为空则更新 lastActiveTime 为当前时间,若为空,则使用当前时间减去 lastActiveTime 得到的值与规定值(假设为1h)做比较,大于 1h 则退出登录。

3.3 代码实现

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')) }

本文转载于:

https://juejin.cn/post/7320044522910269478

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

 记录--前端无感知刷新token & 超时自动退出