当前位置:   article > 正文

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

token 自刷新

一、token的作用

因为http请求是无状态的,是一次性的,请求之间没有任何关系,服务端无法知道请求者的身份,所以需要鉴权,来验证当前用户是否有访问系统的权限。

以oauth2.0授权码模式为例:

89e4a202403071133361293.png

每次请求资源服务器时都会在请求头中添加 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. const HTTPPromise = new Promise((resolve, reject) => {
  9. xhr.onload = function () {
  10. let response;
  11. if (readAs === 'json') {
  12. try {
  13. response = JSONbig.parse(this.responseText || null);
  14. } catch {
  15. response = this.responseText || null;
  16. }
  17. } else if (readAs === 'xml') {
  18. response = this.responseXML
  19. } else {
  20. response = this.responseText
  21. }
  22. resolve({ status: xhr.status, response, getResponseHeader: (name) => xhr.getResponseHeader(name) })
  23. }
  24. xhr.onerror = function () {
  25. reject(xhr)
  26. }
  27. xhr.ontimeout = function () {
  28. reject({ ...xhr, isTimeout: true })
  29. }
  30. beforeSend(xhr)
  31. body ? xhr.send(body) : xhr.send()
  32. xhr.onreadystatechange = function () {
  33. if (xhr.status === 502) {
  34. reject(xhr)
  35. }
  36. }
  37. })
  38. // 允许HTTP请求中断
  39. HTTPPromise.abort = () => xhr.abort()
  40. return HTTPPromise;
  41. }
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. try {
  11. const { status, response, getResponseHeader } = await request
  12. if(status === 401) {
  13. try {
  14. if (!isRefreshing) {
  15. isRefreshing = true
  16. // 刷新token
  17. await refreshToken()
  18. // 按顺序重新发起所有失败的请求
  19. const allRequests = [() => resolve(httpRequest(config)), ...requests]
  20. allRequests.forEach((cb) => cb())
  21. } else {
  22. // 正在刷新token,将请求暂存
  23. requests = [
  24. ...requests,
  25. () => resolve(httpRequest(config)),
  26. ]
  27. }
  28. } catch(err) {
  29. reject(err)
  30. } finally {
  31. isRefreshing = false
  32. requests = []
  33. }
  34. }
  35. } catch(ex) {
  36. reject(ex)
  37. }
  38. })
  39. process.abort = abort
  40. return process
  41. }
  42. // 发起请求
  43. 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. const lastTime = Number(localStorage.get(LastTimeKey))
  48. if (!lastTime || Number.isNaN(lastTime)) {
  49. localStorage.set(LastTimeKey, new Date().getTime())
  50. return
  51. }
  52. const now = new Date().getTime()
  53. const time = now - lastTime
  54. if (time >= keepTime) {
  55. doTimeout()
  56. }
  57. }, IntervalTimeOut)
  58. }
  59. resetTimer()
  60. }
  61. // 上传操作
  62. function upload() {
  63. const current = JSON.parse(localStorage.get(activeEventsKey))
  64. localStorage.set(activeEventsKey, [...current, 'upload'])
  65. ...
  66. // do upload request
  67. ...
  68. const current = JSON.parse(localStorage.get(activeEventsKey))
  69. localStorage.set(activeEventsKey, Array.isArray(current) ? current.filter((item) => itme !== 'upload'))
  70. }
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/744025
推荐阅读
相关标签
  

闽ICP备14008679号