前端主动刷新Token与超时安全退出攻略

宁睿  金牌会员 | 2024-6-25 20:15:10 | 来自手机 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 948|帖子 948|积分 2854

一、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. function httpFactory({ method, url, body, headers, readAs, timeout }) {
  2.     const xhr = new XMLHttpRequest()
  3.     xhr.open(method, url)
  4.     xhr.timeout = isNumber(timeout) ? timeout : 1000 * 60
  5.     if(headers){
  6.         forEach(headers, (value, name) => value && xhr.setRequestHeader(name, value))
  7.     }
  8.    
  9.     const HTTPPromise = new Promise((resolve, reject) => {
  10.         xhr.onload = function () {
  11.             let response;
  12.             if (readAs === 'json') {
  13.                 try {
  14.                     response = JSONbig.parse(this.responseText || null);
  15.                 } catch {
  16.                     response = this.responseText || null;
  17.                 }
  18.             } else if (readAs === 'xml') {
  19.                 response = this.responseXML
  20.             } else {
  21.                 response = this.responseText
  22.             }
  23.             resolve({ status: xhr.status, response, getResponseHeader: (name) => xhr.getResponseHeader(name) })
  24.         }
  25.         xhr.onerror = function () {
  26.             reject(xhr)
  27.         }
  28.         xhr.ontimeout = function () {
  29.             reject({ ...xhr, isTimeout: true })
  30.         }
  31.         beforeSend(xhr)
  32.         body ? xhr.send(body) : xhr.send()
  33.         xhr.onreadystatechange = function () {
  34.             if (xhr.status === 502) {
  35.                 reject(xhr)
  36.             }
  37.         }
  38.     })
  39.     // 允许HTTP请求中断
  40.     HTTPPromise.abort = () => xhr.abort()
  41.     return HTTPPromise;
  42. }
复制代码
2.2.2 无感知刷新token

  1. // 是否正在刷新token的标记
  2. let isRefreshing = false
  3. // 存放因token过期而失败的请求
  4. let requests = []
  5. function httpRequest(config) {
  6.     let abort
  7.     let process = new Promise(async (resolve, reject) => {
  8.         const request = httpFactory({...config, headers: { Authorization: 'Bearer ' + cookie.load('access_token'), ...configs.headers }})
  9.         abort = request.abort
  10.         
  11.         try {                             
  12.             const { status, response, getResponseHeader } = await request
  13.             if(status === 401) {
  14.                 try {
  15.                     if (!isRefreshing) {
  16.                         isRefreshing = true
  17.                         
  18.                         // 刷新token
  19.                         await refreshToken()
  20.                         // 按顺序重新发起所有失败的请求
  21.                         const allRequests = [() => resolve(httpRequest(config)), ...requests]
  22.                         allRequests.forEach((cb) => cb())
  23.                     } else {
  24.                         // 正在刷新token,将请求暂存
  25.                         requests = [
  26.                             ...requests,
  27.                             () => resolve(httpRequest(config)),
  28.                         ]
  29.                     }
  30.                 } catch(err) {
  31.                     reject(err)
  32.                 } finally {
  33.                     isRefreshing = false
  34.                     requests = []
  35.                 }
  36.             }                        
  37.         } catch(ex) {
  38.             reject(ex)
  39.         }
  40.     })
  41.    
  42.     process.abort = abort
  43.     return process
  44. }
  45. // 发起请求
  46. httpRequest({ method: 'get', url: 'http://127.0.0.1:8000/api/v1/getlist' })
复制代码
2.3 Axios 无感知刷新token

  1. // 是否正在刷新token的标记
  2. let isRefreshing = false
  3. let requests: ReadonlyArray<(config: any) => void> = []
  4. // 错误响应拦截
  5. axiosInstance.interceptors.response.use((res) => res, async (err) => {
  6.     if (err.response && err.response.status === 401) {
  7.         try {
  8.             if (!isRefreshing) {
  9.                 isRefreshing = true
  10.                 // 刷新token
  11.                 const { access_token } = await refreshToken()
  12.                 if (access_token) {
  13.                     axiosInstance.defaults.headers.common.Authorization = `Bearer ${access_token}`;
  14.                     requests.forEach((cb) => cb(access_token))
  15.                     requests = []
  16.                     return axiosInstance.request({
  17.                         ...err.config,
  18.                         headers: {
  19.                             ...(err.config.headers || {}),
  20.                             Authorization: `Bearer ${access_token}`,
  21.                         },
  22.                     })
  23.                 }
  24.                 throw err
  25.             }
  26.             return new Promise((resolve) => {
  27.                 // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
  28.                 requests = [
  29.                     ...requests,
  30.                     (token) => resolve(axiosInstance.request({
  31.                         ...err.config,
  32.                         headers: {
  33.                             ...(err.config.headers || {}),
  34.                             Authorization: `Bearer ${token}`,
  35.                         },
  36.                     })),
  37.                 ]
  38.             })
  39.         } catch (e) {
  40.             isRefreshing = false
  41.             throw err
  42.         } finally {
  43.             if (!requests.length) {
  44.                 isRefreshing = false
  45.             }
  46.         }
  47.     } else {
  48.         throw err
  49.     }
  50. })
复制代码
三、长时间无操作超时主动退出

当用户登录之后,长时间不操作应该做主动退出功能,提高用户数据的安全性。
3.1 操作事件

操作事件:用户操作事件紧张包罗鼠标点击、移动、滚动事件和键盘事件等。
特别事件:某些耗时的功能,比如上传、下载等。
3.2 方案

用户在登录页面之后,可以复制成多个标签,在某一个标签有操作,其他标签也不应该主动退出。所以需要标签页之间共享操作信息。这里我们利用 localStorage 来实现跨标签页共享数据。
在 localStorage 存入两个字段:
名称类型说明说明lastActiveTimestring末了一次触发操作事件的时间戳activeEventsstring[ ]特别事件名称数组 当有操作事件时,将当前时间戳存入 lastActiveTime。
当有特别事件时,将特别事件名称存入 activeEvents ,等特别事件竣事后,将该事件移除。
设置定时器,每1分钟获取一次 localStorage 这两个字段,优先判定 activeEvents 是否为空,若不为空则更新 lastActiveTime 为当前时间,若为空,则利用当前时间减去 lastActiveTime 得到的值与规定值(假设为1h)做比较,大于 1h 则退出登录。
3.3 代码实现

  1. const LastTimeKey = 'lastActiveTime'
  2. const activeEventsKey = 'activeEvents'
  3. const debounceWaitTime = 2 * 1000
  4. const IntervalTimeOut = 1 * 60 * 1000
  5. export const updateActivityStatus = debounce(() => {
  6.     localStorage.set(LastTimeKey, new Date().getTime())
  7. }, debounceWaitTime)
  8. /**
  9. * 页面超时未有操作事件退出登录
  10. */
  11. export function timeout(keepTime = 60) {
  12.     document.addEventListener('mousedown', updateActivityStatus)
  13.     document.addEventListener('mouseover', updateActivityStatus)
  14.     document.addEventListener('wheel', updateActivityStatus)
  15.     document.addEventListener('keydown', updateActivityStatus)
  16.     // 定时器
  17.     let timer;
  18.     const doTimeout = () => {
  19.         timer && clearTimeout(timer)
  20.         localStorage.remove(LastTimeKey)
  21.         document.removeEventListener('mousedown', updateActivityStatus)
  22.         document.removeEventListener('mouseover', updateActivityStatus)
  23.         document.removeEventListener('wheel', updateActivityStatus)
  24.         document.removeEventListener('keydown', updateActivityStatus)
  25.         // 注销token,清空session,回到登录页
  26.         logout()
  27.     }
  28.     /**
  29.      * 重置定时器
  30.      */
  31.     function resetTimer() {
  32.         localStorage.set(LastTimeKey, new Date().getTime())
  33.         if (timer) {
  34.             clearInterval(timer)
  35.         }
  36.         timer = setInterval(() => {
  37.             const isSignin = document.cookie.includes('access_token')
  38.             if (!isSignin) {
  39.                 doTimeout()
  40.                 return
  41.             }
  42.             const activeEvents = localStorage.get(activeEventsKey)
  43.             if(!isEmpty(activeEvents)) {
  44.                 localStorage.set(LastTimeKey, new Date().getTime())
  45.                 return
  46.             }
  47.             
  48.             const lastTime = Number(localStorage.get(LastTimeKey))
  49.             if (!lastTime || Number.isNaN(lastTime)) {
  50.                 localStorage.set(LastTimeKey, new Date().getTime())
  51.                 return
  52.             }
  53.             const now = new Date().getTime()
  54.             const time = now - lastTime
  55.             if (time >= keepTime) {
  56.                 doTimeout()
  57.             }
  58.         }, IntervalTimeOut)
  59.     }
  60.     resetTimer()
  61. }
  62. // 上传操作
  63. function upload() {
  64.     const current = JSON.parse(localStorage.get(activeEventsKey))
  65.     localStorage.set(activeEventsKey, [...current, 'upload'])
  66.     ...
  67.     // do upload request
  68.     ...
  69.     const current = JSON.parse(localStorage.get(activeEventsKey))
  70.     localStorage.set(activeEventsKey, Array.isArray(current) ? current.filter((item) => itme !== 'upload'))
  71. }
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

宁睿

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表