首页 > 其他分享 >Token过期处理

Token过期处理

时间:2022-11-28 11:45:48浏览次数:32  
标签:请求 过期 刷新 token 处理 Token error response

Token用于进行接口鉴权,但是Token具有由后端设置的过期时间,当Token过期以后,就无法再请求数据了
项目中后端设置的过期时间为24h,测试时我们可以手动修改token值让Token失效
处理方式:

方式1:用户重新登录,获得新的Token就可以了,但是当过期时间较短的时候,每次都是要重新登录操作 的,体验很差
为了提高用户的信息安全性,Token的过期时间都比较短(就算万一泄露了,过一会儿也就过期无效化了)
方式2:根据用户信息,自动给用户生成新的Token,减少登录次数
我们观察前面的功能的话,接口的响应信息中是有三个和token相关的信息的

access_token:当前使用的token,用于访问需要授权的接口
expires_in:access_token的过期时间
refresh_token:刷新获取新的access_token
刷新Token 的方法有两种:
方法一:
在每个请求发起前进行拦截,根据expires_in判断token是否过期,如果过期则会刷新后再继续请求接口
优点:请求前拦截处理,能节省请求次数
缺点:后端需要提供Token过期时间字段(例如:expires_in),并且需要结合计算机本地时间判断,如果计算机时间被篡改(特别是比服务器时间满)时,拦截会失败的
方法二:
在每个请求响应后进行拦截,如果发现请求失败(Token过期导致的)时,刷新Token再刷新请求接口
优点:无需Token过期时间字段,无需判断时间
缺点:多消耗一次请求
这里推荐使用方法二,相比较下来,方法二更加的稳定,不会出现意外的问题
Axios响应拦截器与错误处理
响应拦截器会在响应接收完毕,在对应请求处理前被拦截器拦截,响应拦截器参数response中保存了相应的信息

// Axios 官方文档:响应拦截器
// Add a response interceptor
axios.interceptors.response.use(function (response) {
  // Any status code that lie within the range of 2xx cause this function to trigger
  // Do something with response data
  return response;
}, function (error) {
  // Any status codes that falls outside the range of 2xx cause this function to trigger
  // Do something with response error
  return Promise.reject(error);
});

那么我们接来下将响应拦截器设置到utils/request.js中,将axios更改为创建的request(因为我们使用了ESLint规范,记得去除所有的分号)

error是需要console.dir()输出的

// utils/request.js
...
// 设置响应拦截器
request.interceptors.response.use(function (response) {
  // 状态码为 2xx 都会进入这里
  console.log('请求响应成功了:', response)
  return response
}, function (error) {
  // 超出 2xx 都会进入这里
  console.dir(error)
  return Promise.reject(error)
})
export default request

Axios错误处理
错误处理,需要在拦截器中找到特定的错误情况进行token刷新
当出现错误时,通过Elemnt的Message组件设置提示,这里我们采用的是引入方式操作

引入的Message与之前使用的this.$message是相同的,只是引入方式与操作方式不同
// 通过局部引入的方式,引入Element的Message组件功能

import { Message } from 'element-ui'

// 响应拦截器
request.interceptors.response.use(function (response) {
  // 状态码2xx会执行这里
  console.log('响应成功了', response)
  return response
}, function (error) {
  if (error.response) {
    // 请求发送成功,响应接收完毕,但是状态码为失败的情况
    // 1.判断失败的状态码情况(主要处理401的情况)
    const { status } = error.response
    let errorMessage = ''
    if (status === 400) {
      errorMessage = '请求参数错误'
    } else if (status === 401) {
      // 2.Token无效(过期)处理
      errorMessage = 'Token 无效'
    } else if (status === 403) {
      errorMessage = '没有权限,请联系管理员'
    } else if (status === 404) {
      errorMessage = '请求资源不存在'
    } else if (status >= 500) {
      errorMessage = '服务器错误,请联系管理员'
    }
    Message.error(errorMessage)
  } else if (error.request) {
    // 请求发送成功,未收到响应
    Message.error('请求超时请重试')
  } else {
    // 意料之外的错误
    Message.error(error.message)
  }
  // 将本次请求的错误对象继续向后抛出,让接收响应的处理函数进行操作
  return Promise.reject(error)
})

刷新Token
HTTP 状态码401表示未授权,导致401的情况有:

没有Token
Token无效
Token过期
判断方法:
检测是否存在refresh_token:(后端通常会限制每个refresh_token只能获取一次新的Token)
如果有,那就通过refresh_token获取新的access_token
获取成功,重启发送请求,请求接口数据就行
获取失败,跳转登录页
如果没有,跳转登录页
由于要进行跳转,在utils/request.js中引入router/index.js

// utils/request.js
// 引入 router
import router from '@/router'

首先要检测store是否有user信息(有就证明是正常登陆,一定存在的有refresh_token),如果存在的有refresh_token的话就请求新的access_token,需要用到对应的刷新接口,接下来检查是否有新的access_token

失败的话,清除用户信息,跳转登录页
跳转登录操作与之前是一致的,建议封装起来
成功的话,更新access_token,同时重新请求之前401的接口

// utils/
...
// 封装跳转登录页面的函数
function redirectLogin () {
  router.push({
    name: 'login',
    query: {
      // router.currentRoute 用于获取当前路由对应的路由信息对象
      redirect: router.currentRoute.fullPath
    }
  })
}

// 设置响应拦截器
request.interceptors.response.use(function (response) {
  ...
}, function (error) {
  // 超出 2xx 都会进入这里
  if (error.response) {
    ...
  } else if (status === 401) {
    if (!store.state.user) {
      /* router.push({
        name: 'login',
        query: {
          // router.currentRoute 用于获取当前路由对应的路由
          redirect: router.currentRoute.fullPath
        }
      }) */
      // 封装函数后更改为调用
      redirectLogin()
      // 阻止后续操作,向下抛出错误对象
      return Promise.reject(error)
    }
    ...
    }).then(res => {
      if (res.data.state !== 1) {
        // 清除已经无效的用户信息
        store.commit('setUser', null)
        // 跳转登录页
        /* router.push({
          name: 'login',
          query: {
            // router.currentRoute 用于获取当前路由对应的路由
            redirect: router.currentRoute.fullPath
          }
        }) */
        // 封装函数后更改为调用
        redirectLogin()
        // 阻止后续操作,向下抛出错误对象
        return Promise.reject(error)
      }
      ...
        }).catch(() => {
        store.commit('setUser', null)
        /* router.push({
          name: 'login',
          query: {
            // router.currentRoute 用于获取当前路由对应的路由
            redirect: router.currentRoute.fullPath
          }
        }) */
        // 封装函数后更改为调用
        redirectLogin()
        return Promise.reject(error)
      })
  } else if (status === 403) {
  ...

处理Token重复刷新
如果页面中存在多个请求(大多数页面中都不会只有一次请求),如果Token过期,每个请求都会刷新Token,这个时候刷新多次都没有意义,又增加了请求个数,还会出现额外的问题

通过浏览器的开发者工具观察,有两次的刷新Token请求,由于两次的刷新token携带的refresh_token相同,会导致一次成功一次失败,失败的那一次会导致页面跳转请求页

为了避免多次请求刷新Token,可以通过一个变量isRefreshing标记Token的刷新状态
默认状态为false,并且在发送刷新Token请求前检测,状态是false才能发送
发送刷新请求的时候,设置标记为true
请求完毕,设置为false

// layout/components/app-header.vue
...
// 是否正在更新 Token
let isRefreshing = false

request.interceptors.response.use(function (response) {
...
  } else if (status === 401) {
    if (!store.state.user) {...}
    // 发送刷新请求前判断 isRefreshing 是否存在其他已发送的刷新请求
    // 1 如果有,则将当前请求挂起,等到 Token 刷新完毕再重发,这里先设置为 return
    if (isRefreshing) {
      return
    }
    // 2. 如果没有,则更新 isRefreshing 并发送请求,继续执行后续操作
    isRefreshing = true
    // 发送刷新请求
    return request({
     ...
    }).then(res => {
      ...
    }).catch(() => {
      ...
    }).finally(() => {
      // 3 请求完毕,无论成功失败,设置 isRefreshing 为 false
      isRefreshing = false
    })
  } else if (status === 403) {
...

虽然刷新Token的问题解决了,但是之前发送的两个请求只有一个成功执行,其他的请求都被阻止了
如何解决?
我们声明一个数组存储所有被挂起的请求,当Token刷新完毕再将这些请求重新发送

// 存储是否正在更新token 的状态
let isRefreshing = false
// 存储因为token刷新而挂起的请求
let requests = []
// 响应拦截器
request.interceptors.response.use(function (response) {
  // 状态码2xx会执行这里
  console.log('响应成功了', response)
  return response
}, function (error) {
  if (error.response) {
    // 请求发送成功,响应接收完毕,但是状态码为失败的情况
    // 1.判断失败的状态码情况(主要处理401的情况)
    const { status } = error.response
    let errorMessage = ''
    if (status === 400) {
      errorMessage = '请求参数错误'
    } else if (status === 401) {
      // 2.Token无效(过期)处理
      // 第一,无token信息
      if (!store.state.user) {
        redirectLogin()
        return Promise.reject(error)
      }
      // 检测是否已经存在了正在刷新token的请求
      if (isRefreshing) {
        // 将当前失败的请求存起来,存储到请求列表中
        return requests.push(() => {
          // 当前函数调用后,会自动发送本次失败请求
          request(error.config)
        })
      }
      isRefreshing = true
      // 第二,Token无效(错误Token,过期Token)
      // 发送请求,获取新的access_token
      return request({
        method: 'POST',
        url: '/front/user/refresh_token',
        data: qs.stringify({
          refreshtoken: store.state.user.refresh_token
        })
      }).then(res => {
        // -刷新token失败
        if (res.data.state !== 1) {
          // 清除无效的用户信息
          store.commit('setUser', null)
          // 封装重复的跳转登录操作
          redirectLogin()
          return Promise.reject(error)
        }
        // 刷新token成功
        // 存储新的token
        store.commit('setUser', res.data.content)
        // 重新发送失败的请求
        // 根据reques
        // 发送多次失败的请求
        requests.forEach(callback => callback())
        // 发送完毕清除requests 内容即可
        requests = []
        // 将本次请求发送
        return request(error.config)
      }).catch(err => {
        console.log(err)
      }).finally(() => {
        // 无论成功还是失败都会执行
        // 请求发送完毕,响应处理完毕,刷新状态更改为false就行了
        isRefreshing = false
      })

解决

标签:请求,过期,刷新,token,处理,Token,error,response
From: https://www.cnblogs.com/Sultan-ST/p/16931763.html

相关文章

  • 基于SqlSugar的开发框架循序渐进介绍(22)-- Vue3+TypeScript的前端工作流模块中实现统一
    在工作流页面中,除了特定的业务表单信息外,往往也需要同时展示通用申请单的相关信息,因此在页面设计的时候需要使用一些组件化的概念来实现动态的内容展示处理,本篇随笔介绍Vue3......
  • Spring Boot异常处理
    SpringBoot异常处理一、SpringBoot默认的异常处理SpringBoot提供了一个默认处理异常的映射。在SpringBoot的Web项目中,尝试访问一个不存在的URL,会得到SpringBoot中......
  • 使用 Asynq 实现 Go 异步任务处理
    # 1.介绍Asynq是一个Go库,用于对任务进行排队并与工作人员异步处理它们。它的工作原理:客户端将任务放入队列服务器从队列中拉出任务并为每个任务启动一个工作......
  • 20220620 DispatcherServlet 处理请求
    概述DispatcherServlet的继承关系DispatcherServlet初始化触发初始化时机处理第一个请求时需要初始化这里执行的是servlet.init方法,也就是Servlet初始化生命周......
  • DRF过滤、排序、异常处理、自定义Response、分页
    DRF过滤、排序、异常处理、自定义Response、分页目录DRF过滤、排序、异常处理、自定义Response、分页过滤局部过滤排序异常处理封装Response对象分页三种分页方式PageNumb......
  • 拓端tecdat|适用于NLP自然语言处理的Python代写:使用Facebook FastText库
    适用于NLP自然语言处理的Python:使用FacebookFastText库 在本文中,我们将研究​​FastText​​,它是用于单词嵌入和文本分类的另一个极其有用的模块......
  • 逻辑过期解决缓存击穿问题
    需求:修改根据id查询商铺的业务,基于逻辑过期方式来解决缓存击穿问题思路分析:当用户开始查询redis时,判断是否命中,如果没有命中则直接返回空数据,不查询数据库,而一旦命中后,将v......
  • 拓端tecdat|R语言编程指导随机波动模型SV处理时间序列中的随机波动率
    使用R语言随机波动模型SV处理时间序列中的随机波动率准备数据采样函数svsample期望其输入数据y是数字矢量,而没有任何缺失值(NA),如果提供其他任何内容,......
  • 异常处理机制
    异常处理机制抛出异常捕获异常异常处理的五个关键字:try,catch,finally,throw,throws其中:throws关键字和throw关键字在使用上的几点区别如下:throws用来声明......
  • 异常处理
    try:except:finally:else:什么时候执行try:print("xxx")#print(1/0)exceptExceptionase:print(e)else:#基本上不会用到print("正常执行,没有出异......