Springboot+Axios双token解决token过期续签问题

2023-10-31

后端分离使用token进行登录验证时,由于token存在过期时间,每次token过期都需要用户重新登录的话,用户体验很不友好。假如token能跟session一样,如果用户持续在进行操作,就自动延长有效时间,就可以解决问题。但是,token一旦签发,服务器就没法再延长token的有效期,目前用的比较多的应该是使用双token实现token续签,当token过期时,签发新的token给前端,前端携带新的token请求后端接口。

具体思路:在签发token时生成两个token,accessToken和refreshToken,前端每次请求时携带accessToken,后端发现accessToken过期时,返回token已过期的结果。前端根据后端状态码判断token是否已经过期,如果过期则携带refreshToken请求刷新token的接口,如果refreshToken没有过期,则后端重新生成accessToken和refreshToken返回给前端;如果refreshToken也过期了,则返回结果要求前端重新登录。

后端实现

accessToken设置过期时间为30分钟,refreshToken设置过期时间为60分钟,这样的话accessToken过期后的30分钟内用户有操作,仍可以使用refreshToken请求刷新token。

创建accessToken

创建refreshToken

JWT解析token,token过期则返回-1,其他解析错误则返回-2,解析成功返回1。

LoginController验证账号密码成功后,创建accessToken和refreshToken返回前端,将accessToken和refreshToken保存在redis中。为避免用户退出登录或更换设备登录后,旧的accessToken和refreshToken还没有过期,仍然能生效,在redis中使用user的id为键保存的accessToken和refreshToken,每次登录后都会将原来的进行覆盖,这样只需要在拦截器中将token与reids中进行比对,如果比对不一致,则不放行。

登录生成accessToken和refreshToken

LoginInterceptor验证accessToken,如果返回-1,则表示accessToken过期,提示用户需要刷新token。为了避免用户在新设备登录,旧设备的accessToken仍然有效,每次校验完accessToken成功,都还要在redis中查找是否存在以id为key的记录,并且将redis中取出的redisToken和accessToken对比是否一致,如果没有或不一致,则表示accessToken已经被redis作废,仍不能放行,返回客户端信息为该账号已在其他设备登录,请重新登录。

代码截不全,主要逻辑都在

refreshToken接口。刷新token的接口/api/refresh用于前端调用。首先刷新token的接口要在Interceptor中放行,避免refreshToken过期后,返回结果仍然是需要刷新token。只有refreshToken解析成功并且与redis中的refreshToken一致时,才会重新签发accessToken和refreshToken。

刷新token的接口

前端实现

前端实现靠axios的请求拦截器和响应拦截器,请求拦截器配置每次请求携带token,主要的难题在于多请求下响应拦截器的处理。

具体思路:设置一个刷新token的状态isRefreshAvailable并设置为true,同时发出多个请求时,token过期都会由后端返回需要刷新token的信息,那么,当第一个响应回来进入刷新token程序后,将isRefreshAvailable设置为false,其他请求都不能再发起刷新token请求,使用promise将剩下的请求放入一个缓存数组,当刷新token结束后再遍历数组将缓存的请求逐个发出。

由于前端知识不足,网上查了不少办法,主要出现两个问题,问题的分析不知道是否正确,最后用了setTimeout延迟3秒再将isRefreshAvailable设置为true并且重新发送缓存的请求,没发现再出现以下两个问题。如果有好的解决办法,请不吝赐教,万分感激

问题1、刷新token接口多次被调用。调用了7个请求系统时间接口的请求,按照网上的办法,调用刷新token接口得到新的accessToken和refreshToken后就将isRefreshAvailable设置为true,但有的原始请求响应晚于刷新token的请求响应,造成多次调用刷新token接口,而后端即便token解析成功也会从redis中进行比对,造成重发的请求携带的accessToken与redis中不一致,比对失败返回重新登录页面。解决问题的关键在于何时改变isRefreshAvailable的状态。

问题2、请求丢失的问题。原始请求因为返回结果较晚,当刷新完token开始遍历缓存数组的时候,有的原始请求结果才返回,这样即便进了数组,也没有能够重新发送。

发送了7个系统时间请求,刷新token后只重发了2个

前端代码:

accessToken和refreshToken存放在sessionStorage中,获取accessToken和refreshToken的以及清空sessionStorage到登录页面的函数:

function getAccessToken () {
  return window.sessionStorage.getItem('token')
}

function getRefreshToken () {
  return window.sessionStorage.getItem('refreshToken')
}

function toLogin () {
  setTimeout(() => {
    window.sessionStorage.clear()
    isRefreshAvailable = true
    requestAttr = []
    window.location.href = '/login'
  }, 3000)
}

刷新token的函数:获得刷新后的accessToken和refreshToken后,保存到sessionStorage中,得到新的token后这里先不设置isRefreshAvailable为ture。

刷新token函数

async function refreshToken () {
  try {
    var result = await http({
      url: '/test/refresh',
      method: 'post',
      headers: {
        Refresh: getRefreshToken()
      }
    })
  } catch (e) {
    messageOnce.error({ message: '自动获取授权失败! 3秒后自动跳转至登录界面' })
    toLogin()
  }
  if (result.status === 200) {
    window.sessionStorage.setItem('token', result.accessToken)
    window.sessionStorage.setItem('refreshToken', result.refreshToken)
  }
}

请求拦截器:每次请求都在请求头中设置Authorization字段携带token,这里使用了element ui的Loading加载组件,为了确保所有的ajax请求响应后再关闭Loading,使用了loadCount进行计数,每发起一个请求,loadCount加1。

请求拦截器

http.interceptors.request.use(
  config => {
    var token = getAccessToken()
    token && (config.headers.Authorization = token)
    loadCount++
    loadingInstance = Loading.service({
      text: '正在加载...'
    })
    return config
  },
  error => {
    loadingInstance.close()
    messageOnce.warning({ message: '请求超时' })
    return Promise.reject(error)
  }
)
// 是否可以刷新标识
let isRefreshAvailable = true
// 缓存请求的数组
let requestAttr = []

响应拦截器:当后端响应token相关错误的状态码时,10001代表没有token,10002代表token解析失败,10003代表refreshToken过期,清空sessionStorage并自动跳转至登录界面。这里每有一个请求得到响应,就将loadCount减1,当loadCount为0,且isRefreshAvailable为true时,关闭Loading组件。当后端响应accessToken过期的10000时,根据isRefreshAvailable判断是否正在刷新token,isRefreshAvailable为true,表示可以刷新token,调用刷新token的refreshToken函数,并将isRefreshAvailable设置为false,其他响应不能再调用refreshToken函数。为了避免前述的token多次刷新和请求丢失的两个问题,刷新完token,3秒后再将缓存数组中的请求进行重发,并且将isRefreshAvailable设置为true。

响应拦截器

如果其他token过期的响应回来时正在刷新token,则使用promise将请求存入缓存数组requestAttr,如果不是token相关的错误状态码,则打印错误结果,如果状态码为成功200,则将响应数据返回。

响应拦截器

http.interceptors.response.use(
  response => {
    loadCount--
    if (loadCount === 0 && isRefreshAvailable === true) {
      loadingInstance.close()
    }
    if (response.data.status === 10001 || response.data.status === 10002 || response.data.status === 10003) {
      messageOnce.error({ message: response.data.message + '! 3秒后自动跳转至登录界面' })
      toLogin()
    } else if (response.data.status === 10000) {
      if (isRefreshAvailable) {
        isRefreshAvailable = false
        refreshToken()
        // 拿到新accessToken后,等待2-3秒,确保其他请求响应都回来后再重新发送请求
        // 防止重发数组请求后才有请求返回,丢失该部分请求
        setTimeout(() => {
          console.log('开始重新发起请求')
          requestAttr.forEach((cb) => cb(getAccessToken()))
          requestAttr = []
          isRefreshAvailable = true
          response.config.headers.Authorization = 'Bearer' + getAccessToken()
          return http(response.config)
        }, 3000)
      } else {
        return new Promise(resolve => {
          requestAttr.push((token) => {
            console.log('缓存数组的数量:', requestAttr.length)
            response.config.headers.Authorization = 'Bearer' + token
            resolve(http(response.config))
          })
        })
      }
    } else if (response.data.status !== 200) {
      messageOnce.warning({ message: response.data.message })
    } else {
      return response.data
    }
  },
  error => {
    // 对响应错误做点什么
    loadCount = 0
    loadingInstance.close()
    messageOnce.error({ message: '与服务器连接发生错误' })
    return Promise.resolve(error)
  }
)

最终效果,发出7个请求系统时间的请求,得到7个需要刷新token的响应,只调用了一次refresh接口,又重新发送了7个请求系统时间的请求。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Springboot+Axios双token解决token过期续签问题 的相关文章

随机推荐

  • C++设计模式(二)(摘录)

    C 设计模式 二 摘录 装饰模式 装饰模式 动态地给一个对象添加一些额外的功能 它是通过创建一个包装对象 也就是装饰来包裹真实的对象 新增加功能来说 装饰器模式比生产子类更加灵活 以下情形考虑使用装饰模式 需要扩展一个类的功能 或给一个类添
  • java之Thread类详细分析(全)

    目录 前言 1 属性值 1 1 线程属性方法 2 常用方法 3 使用方法 前言 Thread是程序中的执行线程 jvm并发地运行多个执行线程 1 属性值 通过查看Thread源码 是继承Runnable接口的实现类 无论使用Runnable
  • [Java]学习笔记

    一些JAVA的学习笔记 记录 主要是和C的不同点 https www runoob com java java tutorial html 声明数组变量 注意 建议使用 dataType arrayRefVar 的声明风格声明数组变量 da
  • 【多同步挤压变换】基于多同步挤压变换处理时变信号和噪声信号研究(Matlab代码实现)

    欢迎来到本博客 博主优势 博客内容尽量做到思维缜密 逻辑清晰 为了方便读者 座右铭 行百里者 半于九十 本文目录如下 目录 1 概述 2 运行结果 2 1 算例1 2 2 算例2 2 3 算例3 2 4 算例4 2 5 算例5 3 参考文献
  • c++版本itk读取dicom序列

    读取dicom序列 hablee void readDicomSeries const std string dicom dir itk Image
  • .NET MVC+EF CodeFirst+IOC+EasyUI 框架设计教程(概述)

    一 开发环境 Win7 VS2013 C SQL Server2008 R2 二 总体源代码结构如下 说明 这个分层按个人理解 不一定要照我的 如果做过开发 会三层架构之类的理解起来就容易 01 Infrastructure 基础构件 Ne
  • QGIS批量将分幅遥感影像合并

    要在QGIS中编程实现批量将分幅遥感影像拼接成完整影像 可以按照以下步骤进行操作 1 首先 确保已经安装并正确配置了QGIS Python环境 并导入所需的库 以下是代码的导入部分 from qgis core import QgsProj
  • 常见的金融术语——176个基础金融知识名词解释

    基金类 1 开放式基金 open end funds 指基金规模不是固定不变的 而是可以随时根据市场供求情况发行新份额或被投资人赎回的投资基金 2 封闭式基金 close end funds 指基金规模在发行前已确定 在发行完毕后及规定的期
  • 解决文字自动换行,字母、数字不换行问题

    添加css属性word break break all
  • mysql修改root密码的语句

    ALTER USER root localhost IDENTIFIED WITH mysql native password BY 新密码 其中 localhost 字符 有的小伙伴可能遇到过无法通过navicat连接Linux中的MyS
  • 单点登录SSO:可一键运行的完整代码

    单点登录方案不同于一个普通站点 它的部署比较繁琐 涉及到好几个站点 要改host 安装证书 配置HTTPS 看到的不少这方面示例都是基于HTTP的 不认同这种简化 1 它体现不出混合HTTP HTTPS时 单点注销要注意处理的问题 2 做单
  • 【Node.js】下载安装及简单使用

    说起Node js 它是当前市面上非常受欢迎的框架 允许我们使用JavaScript搭建后端应用 它有着种种优点 诸如 非阻塞I O 事件驱动 跨平台 高性能 单线程 等等等等 不过现在我们不必执拗与关心这些优点的含义 当务之急是先上手他
  • conda加速设置

    Conda作为使用最为便捷的python环境管理工具 可以协助我们很方便的下载安装第三方库 软件包等操作 但其在下载资源的过程中速度不言而喻 尤其是在更换国内源的情况下 下载速度没有实质性的改变是很令人头疼的一件事 Mamba 树眼镜蛇 能
  • (tensorflow学习)用Object Detection API实现摄像头实时物体检测

    对于物体识别 谷歌已经有训练好的模型供我们使用 图方便不想自己训练的可以直接使用 说实话 装这个tensorflow真心麻烦 我建议用anaconda环境搭建 还要注意装的话装1 几的版本就可 用gpu跑的话注意显卡型号和版本是否兼容 真是
  • 【C++】内存管理

    目录 一 C C 内存分布 二 C语言中动态内存管理方式 三 C 中动态内存管理 1 开辟空间 2 释放空间 四 operator new与operator delete函数 五 内存泄漏 1 什么是内存泄漏 2 如何避免内存泄漏 总结 一
  • Python的getattr方法

    getattr是Python中的内置函数 用于获取一个对象的属性值 这个函数是动态获取属性的一种方式 特别适用于你事先不知道要获取哪个属性 或者属性名是在运行时确定的情况 使用方法 getattr object name default o
  • 资产安全 错题点

    数据所有者 1 决定谁有权访问信息系统 2 对资产负有最终责任 PS 对资产负有最终责任的 高级管理层 数据所有者 首选管理层 3 行为规则 制定规则 以便用于主体的数据或信息的适当使用及保护 4 决定数据的级别 每年回顾确保数据分级的正确
  • 【国产化踩坑记】openEuler系统安装,nvidia驱动,cuda,anaconda安装步骤记录

    1 openEuler安装步骤 尝试安装了openEuler20 03和22 03两个版本 在摸索的过程中总结了一下步骤 以及相关问题的解决方案 进行简单记录 便于后续使用 1 openEuler20 03安装步骤 网络配置以及可视化操作界
  • Segmentation fault (core dumped) 错误的一种解决场景

    错误类型 Segmentation fault core dumped 产生原因 Segmentation fault 段错误 Core Dump 核心转储 是操作系统在进程收到某些信号而终止运行时 将此时进程地址空间的内容以及有关进程状态
  • Springboot+Axios双token解决token过期续签问题

    后端分离使用token进行登录验证时 由于token存在过期时间 每次token过期都需要用户重新登录的话 用户体验很不友好 假如token能跟session一样 如果用户持续在进行操作 就自动延长有效时间 就可以解决问题 但是 token