记录--前端无感知刷新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工厂函数
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
如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

记录--前端无感知刷新token & 超时自动退出的更多相关文章
- 基于OAuth2.0的token无感知刷新
目前手头的vue项目关于权限一块有一个需求,其实架构师很早就要求我做了,但是由于这个紧急程度不是很高,最近临近项目上线,我才想起,于是赶紧补上这个功能.这个项目是基于OAuth2.0认证,需要在每个请 ...
- OAuth2.0与前端无感知token刷新实现
前言 OAuth是一个关于授权(authorization)的开放网络标准,在全世界得到广泛的应用.Facebook.Twitter和Google等各种在线服务都提供了基于OAuth规范的认证机制. ...
- 无感刷新 Token
什么是JWT JWT是全称是JSON WEB TOKEN,是一个开放标准,用于将各方数据信息作为JSON格式进行对象传递,可以对数据进行可选的数字加密,可使用RSA或ECDSA进行公钥/私钥签名. 使 ...
- 设置Linux shell超时自动退出
Linux shell,一般默认情况下是不会超时退出的,但是有的时候我们想要让它在多少分钟后没有操作自动退出终端(听起来有点像windows多少分钟后自动锁屏一样).我们可以通过设置来实现这一功能. ...
- Linux登录超时自动退出处理办法
出于安全方面的考虑,机器常要求配置一个登录时间期限,当闲置超过这一期限就自动退出:但在某些场合我们需要时不时地就使用机器,如果每次都要重新ssh登录那是非常麻烦的 方法一:让当前会话一直处于工作状态 ...
- ASP.NET MVC (Umbraco)中如何设置网站超时自动退出
原文章请参考 https://edgewebware.com/2014/06/automatically-log-out-members-send-login-page-umbraco/ 在网站开发 ...
- web页面超时自动退出方法
思路: 使用 mousemover 事件来监测是否有用户操作页面,写一个定时器间隔特定时间检测是否长时间未操作页面,如果是,退出: 具体时间代码如下(js):var lastTime = new Da ...
- mysql 8/oracle 登录失败处理,应配置并启用结束会话、限制非法登录次数和当登录连接超时自动退出等相关措施
1 mysql 8 先安装密码插件 install plugin CONNECTION_CONTROL soname 'connection_control.so';install plugin CO ...
- 登录超时自动退出,计算时间差-b
// 此方法适用于所有被创建过的controller,且当前controller生命周期存在,如有错误的地方望大神斧正 // 说一下我们的需求和实现原理,需求:在点击home键退出但没有滑飞它,5分 ...
- Spring Cloud实战 | 最八篇:Spring Cloud +Spring Security OAuth2+ Axios前后端分离模式下无感刷新实现JWT续期
一. 前言 记得上一篇Spring Cloud的文章关于如何使JWT失效进行了理论结合代码实践的说明,想当然的以为那篇会是基于Spring Cloud统一认证架构系列的最终篇.但关于JWT另外还有一个 ...
随机推荐
- MySQL百万级数据大分页查询优化的实现
前言:在数据库开发过程中我们经常会使用分页,核心技术是使用用limit start, count分页语句进行数据的读取. 一.MySQL分页起点越大查询速度越慢 直接用limit start, cou ...
- 使用python进行视频图片提取
操作系统 : Windows 10 [版本 10.0.19043.1165] Python 版本 : 3.9.2_x64 可以借助python代码使用opencv实现,命令行示例代码如下: # pyt ...
- NC15291 幸运数字Ⅱ
题目链接 题目 题目描述 定义一个数字为幸运数字当且仅当它的所有数位都是4或者7. 比如说,47.744.4都是幸运数字而5.17.467都不是. 定义next(x)为大于等于x的第一个幸运数字.给定 ...
- 基于 log4j2 插件实现统一日志脱敏,性能远超正则替换
前言 金融用户敏感数据如何优雅地实现脱敏? 日志脱敏之后,无法根据信息快速定位怎么办? 经过了这两篇文章之后,我们对日志脱敏应该有了一定的理解. 但是实际项目中,我们遇到的情况往往更加复杂: 1)项目 ...
- Set与WeakSet
Set与WeakSet Set对象允许存储任何类型的唯一值,无论是原始值或者是对象引用,Set对象中的值不会重复. WeakSet对象允许存储对象弱引用的唯一值,WeakSet对象中的值同样不会重复, ...
- Vulnhub内网渗透DC-7靶场通关
个人博客: xzajyjs.cn DC系列共9个靶场,本次来试玩一下一个 DC-7,下载地址. 下载下来后是 .ova 格式,建议使用vitualbox进行搭建,vmware可能存在兼容性问题.靶场推 ...
- 公司官网建站笔记(二):在云服务器部署PHP服务(公网访问首页)
前言 上一篇重新安装了CentOS8.2之后,接下来开始安装部署PHP服务器,让公网可以访问到我们部署的PHP服务器首页. 背景 为什么自行搭建,是因为红胖子专业做相关Qt软件以及终端设备 ...
- MySQL和Redis基本安装和配置
MySQL 下载和安装 mysql官网下载:https://dev.mysql.com/downloads/mysql/ 下载后将目录下的bin路径加入到环境变量中 在安装目录下创建 my.ini 配 ...
- Simulink模型指标分析与模型重构的最佳实践 - 软件模型质量保证不可忽视的一环
在基于模型的开发中,优质的模型架构是生成优质代码的必要前提.静态模型分析对于模型的质量保证有着至关重要的作用,同时建模规范已在业内有着广泛而成熟的应用.然而建模规范并非模型设计原则合规性的唯一考量标准 ...
- SpringCloud使用Kafka消费者
目录 POM文件配置 创建kafka配置 系统配置信息 启动入口 POM文件配置 <project xmlns="http://maven.apache.org/POM/4.0.0&q ...
