首页 > 其他分享 >记录--前端无感知刷新token & 超时自动退出

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

时间:2024-01-05 18:11:22浏览次数:30  
标签:... const -- xhr token localStorage 刷新 超时

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

前端无感知刷新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工厂函数

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 存入两个字段:

当有操作事件时,将当前时间戳存入 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

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

 

标签:...,const,--,xhr,token,localStorage,刷新,超时
From: https://www.cnblogs.com/smileZAZ/p/17947799

相关文章

  • Spring总结
    Spring框架1、简介Spring:春天---->给软件行业带来了春天2002:首次推出了Spring框架的雏形spring框架即以interface21框架为基础,经过重新设计,并不断丰富其内涵,于2004年3月24日发布1.0正式版.RodJohnson,springframework创始人,很难想象这个人的学历,他学音乐学Sprin......
  • 点餐系统源码(小程序+APP+H5)-外卖-点餐-餐饮
     PHP点餐系统是餐营业管理的“机械”部分。它们是获取我们的预测、实际订单、安全库存和订单数量并将其转换为采购订单或生产订单的程序。由于其机械性质,订购系统并没有太多理论。但这并不意味着您不需要了解一些事情。PHP点餐系统是一种基于Web的应用程序,旨在帮助餐厅和餐馆管......
  • 你的数智化底座物尽其用了吗?
    作者|郑思宇在数智化转型过程中,构建具备领先技术能力,能够与企业业务充分融合的数智底座是企业取得转型成功的重要前提条件。但数智底座建成后,这个平台的使命并不意味着已经完成。一方面,平台需要动态且及时的适配企业不断变化的业务需求,以更少的投入开发出新的企业应用。另外一方......
  • linux下LVM逻辑卷的建立、扩容和缩容
    ---------建立逻辑卷---------1.新建2个分区,sda5 5G,sd610G,完成之后如下[root@yangcan/]#fdisk-lDisk/dev/sda:42.9GB,42949672960bytes255heads,63sectors/track,5221cylindersUnits=cylindersof16065*512=8225280bytesSectorsize(logical/physical......
  • 浪潮服务器某根内存容量减半-32G变成16G
    服务器某根内存条内存容量减半;内存配置:32G*16根问题:CPU0_C2D0槽位内存显示为16G型号:三星32GDDR43200MHz 停机更换后恢复正常......
  • 乱套了!晶圆代工降价竞争白热化!降幅最高15% | 百能云芯
    随着半导体产业的不确定性和市况回落,晶圆代工市场再次掀起波澜,“降价大军”再填猛将。据综合媒体报道,传三星计划在2024年第一季度调降晶圆代工报价,提供5%至15%的折扣,并表示愿意进一步协商。台积电根据客户的投产量进行定制化折扣,具体折扣范围并未透露。联电透露,8英寸晶圆将经历明显......
  • 如何为OpenHarmony贡献(10):论英文资料的风格与基调
    基调定义了我们通过资料与开发者进行沟通的方式。OH开发者英文资料应遵循简明、可信、有温度的基调,体现良好的包容性,从语言文化的角度去连接并使能开发者,助力开发者生态的全球拓展和繁荣。在任何面向开发者的英文资料,我们应遵循统一的基调。三大准则开发者英文资料的语调应遵循以下......
  • 6本报告,助你2024招聘「才」源滚滚!
    新的一年已经拉开序幕,面对全球经济的不确定性,,各行各业都在热切关注着他们未来的发展前景,人力资源行业正站在一个崭新的起点。在这个充满挑战与机遇的时代,技术进步的速度令人叹为观止,企业对技术创新和人才招聘的重视将进一步提升......在各种因素的共同作用下,新的一年将迎来哪些变革......
  • 联想FinOps实践与平台工具建设之路及专家研讨
    联想FinOps实践与平台工具建设之路及专家研讨2024-01-0517:30·优维科技今年以来,我们举办了多期FinOps的专题分享,邀请了美图、腾讯、B站、趣丸、知乎等厂商和行业专家,分享他们在FinOps领域的经验。我们也发现越来越多的人对FinOps产生了浓厚的兴趣,而且FinOps的成熟度也在逐渐提升......
  • 北邮Android大作业,仿抖音APP+源代码+文档说明+答辩ppt+演示视频
    项目介绍apk文件在本文件夹下,可以安装并进行预览。apkfileisunderthisfolder,youcandownloadandtakeapreviewifyouwant.PPT也在本文件夹下界面预览项目备注1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用!2、本项目适合计算机相关专业......