关于无感刷新token,我也有话说

2,886 阅读3分钟

原文 | 奇思站 (qszone.com)

最近在掘金看到一篇讲无感刷新token的文章,正好最近我的个人网站奇思Admin也处理了一个刷新token的问题,而我的解决方案跟这篇文章讲到的所有方案都不一样

所以,我也想借这个话题来写(shui)一写(shui),于是,就有了这篇文章

其实jwt早就不是一个新鲜的词了,你可能没用过,但多少肯定了解过,所以这里就不赘述jwt的概念了,想了解的可以自行google或者百度

为什么需要刷新token

测试:前端,快帮我看一下我这页面怎么突然就跳到登录页了呢,我还没测嗨呢
前端:我看看...哦,是token过期了
测试:??这不合理啊,明明我前一秒还在操作呢
前端:我想想...
...
前端:后端,你能不能把给我的token过期时间设置的长一点
后端:不行啊,这样做不安全,但我可以给你一个刷新token的接口,这样你就可以拿到一个重新计时的新token
前端:好吧,我想想...

怎么刷新token

来看一下这篇文章的作者讲到了哪几种方案

方法一

方法二

个人觉得方法二其实还是比较稳妥的,所谓的资源浪费和性能消耗指的就是定时器,但这点性能消耗其实并不多,完全没必要看这么重

方法三
这个方法是这篇文章的作者自己提供并使用的方案,并给出了详细的实现代码,感兴趣的可以看下原文

个人觉得这个方案是上面几种方案中最不可取的,因为这个方案方式为了防止token过期后同时发起两个或者两个以上的请求,将请求存进了队列中,并通过Promise的Pending状态阻塞多个请求,这不仅增加代码复杂度,还很不优雅,最重要的是token过期后会让接口响应时间延长,容易造成请求超时,用户其实是很容易感知到的,体验并不好

本人使用的方案

通过函数节流的方式刷新token,在请求拦截器中去尝试调用刷新token的方法,如果和上次刷新token的时间间隔不够则不刷新token,这样的话可以保证每次F5刷新或者进入页面的时候刷新一次token,在页面持续操作也能保证每隔一段时间刷新一次token,离开页面后也不会继续刷新token,基本不会有太大的开销,话不多说,上代码

/utils/index.js

/**
 * 节流函数
 * @param {Function} fn 
 * @param {Number} wait 
 * @returns {Function}
 */
export function throttle(fn, wait) {
  var context, args
  var previous = 0

  return function () {
    var now = +new Date()
    context = this
    args = arguments
    if (now - previous > wait) {
      fn.apply(context, args)
      previous = now
    }
  }
}

/utils/cookie.js

import Cookies from 'js-cookie'
import { refreshToken } from '@/api/auth'
import { throttle } from '@/utils'

const TOKEN_CODE = 'access_token'

export function setToken(token) {
  return Cookies.set(TOKEN_CODE, token, { expires: new Date(new Date().getTime() + 2 * 60 * 60 * 1000) })
}

// 防止频繁刷新token,对此方法节流处理,30分钟间隔
export const refreshAccessToken = throttle(async function () {
  try {
    // 请求刷新token接口
    const res = await refreshToken()
    if (res.code === 0) {
      setToken(res.data.token)
    }
  } catch (error) {
    console.error(error)
  }
}, 1000 * 60 * 30)

/utils/request.js

import axios from 'axios'

const service = axios.create({
  timeout: 120000,
  baseURL: '/',
})

service.interceptors.request.use(
  async config => {
    // 排除登录、注册、刷新token等请求
    if (!config.url.startsWith('/auth')) {
      refreshAccessToken()
    }
    return config
  },
  err => Promise.reject(err)
)

这就是我在我的个人网站中的实现方式了,个人感觉还是挺优雅简单的

注意:这种实现方式有个隐藏的问题,就是如果用户是需要进行长时间操作但又不涉及请求接口,则有可能会无法及时刷新token造成token过期,当然要规避这种情况也是可以的,最简单的规避方式就是将token过期时间适当的延长一点,比如延长至12小时或者更久(不建议超过24小时)