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


    后端分离使用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到登录页面的函数:

    1. function getAccessToken () {
    2. return window.sessionStorage.getItem('token')
    3. }
    4. function getRefreshToken () {
    5. return window.sessionStorage.getItem('refreshToken')
    6. }
    7. function toLogin () {
    8. setTimeout(() => {
    9. window.sessionStorage.clear()
    10. isRefreshAvailable = true
    11. requestAttr = []
    12. window.location.href = '/login'
    13. }, 3000)
    14. }

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

    刷新token函数

    1. async function refreshToken () {
    2. try {
    3. var result = await http({
    4. url: '/test/refresh',
    5. method: 'post',
    6. headers: {
    7. Refresh: getRefreshToken()
    8. }
    9. })
    10. } catch (e) {
    11. messageOnce.error({ message: '自动获取授权失败! 3秒后自动跳转至登录界面' })
    12. toLogin()
    13. }
    14. if (result.status === 200) {
    15. window.sessionStorage.setItem('token', result.accessToken)
    16. window.sessionStorage.setItem('refreshToken', result.refreshToken)
    17. }
    18. }

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

    请求拦截器

    1. http.interceptors.request.use(
    2. config => {
    3. var token = getAccessToken()
    4. token && (config.headers.Authorization = token)
    5. loadCount++
    6. loadingInstance = Loading.service({
    7. text: '正在加载...'
    8. })
    9. return config
    10. },
    11. error => {
    12. loadingInstance.close()
    13. messageOnce.warning({ message: '请求超时' })
    14. return Promise.reject(error)
    15. }
    16. )
    17. // 是否可以刷新标识
    18. let isRefreshAvailable = true
    19. // 缓存请求的数组
    20. 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,则将响应数据返回。

    响应拦截器

    1. http.interceptors.response.use(
    2. response => {
    3. loadCount--
    4. if (loadCount === 0 && isRefreshAvailable === true) {
    5. loadingInstance.close()
    6. }
    7. if (response.data.status === 10001 || response.data.status === 10002 || response.data.status === 10003) {
    8. messageOnce.error({ message: response.data.message + '! 3秒后自动跳转至登录界面' })
    9. toLogin()
    10. } else if (response.data.status === 10000) {
    11. if (isRefreshAvailable) {
    12. isRefreshAvailable = false
    13. refreshToken()
    14. // 拿到新accessToken后,等待2-3秒,确保其他请求响应都回来后再重新发送请求
    15. // 防止重发数组请求后才有请求返回,丢失该部分请求
    16. setTimeout(() => {
    17. console.log('开始重新发起请求')
    18. requestAttr.forEach((cb) => cb(getAccessToken()))
    19. requestAttr = []
    20. isRefreshAvailable = true
    21. response.config.headers.Authorization = 'Bearer' + getAccessToken()
    22. return http(response.config)
    23. }, 3000)
    24. } else {
    25. return new Promise(resolve => {
    26. requestAttr.push((token) => {
    27. console.log('缓存数组的数量:', requestAttr.length)
    28. response.config.headers.Authorization = 'Bearer' + token
    29. resolve(http(response.config))
    30. })
    31. })
    32. }
    33. } else if (response.data.status !== 200) {
    34. messageOnce.warning({ message: response.data.message })
    35. } else {
    36. return response.data
    37. }
    38. },
    39. error => {
    40. // 对响应错误做点什么
    41. loadCount = 0
    42. loadingInstance.close()
    43. messageOnce.error({ message: '与服务器连接发生错误' })
    44. return Promise.resolve(error)
    45. }
    46. )

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

  • 相关阅读:
    谁能拒绝摸鱼的时候来点代码图呢
    Win10安装Anaconda和VSCode
    批处理中的%~语法
    如何摆脱自卑心理,自我提升和自我接纳是关键
    为什么插入排序比冒泡排序更受欢迎?
    分布式链路追踪技术 Sleuth +Zipkin
    Redis线程模型
    测试开发如何设计测试用例
    MacOS Ventura 13 优化配置(ARM架构新手向导)
    中小学生使用全光谱台灯对眼睛好不好?2023口碑好的护眼台灯推荐
  • 原文地址:https://blog.csdn.net/huang714/article/details/127786678