You need to enable JavaScript to run this app. React前端如何基于后端refreshToken实现1分钟有效期的accessToken自动刷新
优惠活动
大模型
产品
解决方案
定价
更多

React前端如何基于后端refreshToken实现1分钟有效期的accessToken自动刷新

React 端自动刷新Access Token的规范实现方案

先点破一个大家最容易踩的坑:上来就写个固定60秒触发的setInterval做token刷新,本质上是完全不规范的。结合你后端固定1分钟有效期的accessToken、独立refreshToken换发接口的设计,最合理的实现逻辑是基于token剩余有效期动态触发 + 单例刷新锁防并发请求 + 多场景异常兜底,具体落地方式如下:

核心设计原则

  • 避免无效重复刷新:不要不管当前token是否可用,到点就硬调刷新接口
  • 避免过期请求漏网:不要等后端返回401了才被动触发刷新
  • 解决多标签页、页面切后台导致的定时器失效问题

具体落地实现步骤

  • 统一封装Token存储模块
    不要在各个业务组件里零散操作本地存储,把token的读写、过期时间管理统一收敛到单独的工具模块里:

    • 持久化存储两个核心值:accessToken、accessTokenExpireAt(这个值不需要后端返回,前端在拿到accessToken的时候直接计算:当前时间戳 + 60*1000,因为后端固定accessToken是1分钟有效期)
    • refreshToken根据部署场景调整存储方式:同域前后端优先存在HttpOnly Cookie里避免XSS窃取;跨域场景存在localStorage时,要做好多标签页的同步逻辑。
  • 实现带锁的单例刷新函数
    写专门的刷新方法,通过状态锁保证整个页面同一时间只会发一次刷新请求,其他等待的请求会在刷新完成后复用结果,不会出现并发调用把refreshToken刷失效的问题:

    let isRefreshing = false
    const pendingCallbacks = []
    
    async function refreshAccessToken() {
      // 已有刷新请求在跑的话,把当前调用挂起,等刷新完拿结果
      if (isRefreshing) {
        return new Promise(resolve => pendingCallbacks.push(resolve))
      }
      isRefreshing = true
      try {
        const res = await fetch('/Token', {
          method: 'POST',
          body: JSON.stringify({ refreshToken: getRefreshToken() })
        })
        const newAccessToken = res.data.accessToken
        // 写入新的token和对应的过期时间
        saveAccessToken(newAccessToken, Date.now() + 60 * 1000)
        // 释放所有挂起的调用
        pendingCallbacks.forEach(cb => cb(newAccessToken))
        pendingCallbacks.length = 0
        return newAccessToken
      } finally {
        isRefreshing = false
      }
    }
    
  • 动态调度刷新定时器
    在用户完成SignIn/Login拿到第一组token后,不要启动永久运行的全局定时器,而是根据token的过期时间,动态计算下一次刷新的等待间隔,提前5秒触发刷新(预留网络请求的时间差,避免刚好卡着过期点发请求):

    let refreshTimer = null
    function scheduleTokenRefresh(expireAt) {
      // 清除上一次的定时器,避免定时器叠加
      if (refreshTimer) clearTimeout(refreshTimer)
      // 计算等待时长:过期时间 - 提前刷新的5秒 - 当前时间,最小等待时间为0
      const waitDuration = Math.max(expireAt - 5 * 1000 - Date.now(), 0)
      refreshTimer = setTimeout(async () => {
        try {
          await refreshAccessToken()
          // 刷新成功后,递归调度下一次刷新
          scheduleTokenRefresh(getAccessTokenExpireTime())
        } catch (err) {
          // 刷新失败说明refreshToken已失效,执行登出逻辑清除本地凭证
          triggerLogout()
        }
      }, waitDuration)
    }
    
  • 请求拦截层兜底
    在统一的请求封装层加拦截判断:每个发往后端的请求发出前,先检查当前accessToken的状态——如果已经过期,就先等待刷新完成拿到新token,再带着新token把原请求发出去,不要等后端返回401再做补救。

  • 异常场景兼容

    • 页面切回前台的检查:浏览器对后台页面的定时器会做节流,当用户从后台切回当前页面时,主动检查一次当前token的过期状态,如果已经过期就立刻触发刷新,不要等定时器自然唤醒。
    • 多标签页同步:监听storage事件,当其他标签页更新了本地存储里的accessToken和过期时间,当前标签页同步更新本地的token状态,避免重复调用刷新接口。

常见非规范实现的问题

  • 用固定间隔的setInterval每60秒刷一次:很容易因为请求耗时、浏览器定时器节流的问题,出现带着过期token发请求的情况;而且即使用户已经退出登录,只要页面没关,定时器还会在后台空跑。
  • 只在401响应拦截器里做被动刷新:会导致用户的第一次请求带着过期token发出去,后端返回401,前端再重发一次,不仅有额外的请求延迟,还可能遇到非幂等接口重复提交的问题。
  • 把刷新逻辑散在各个业务组件里:容易出现重复调用刷新接口、未登录状态下误触发刷新的问题。

内容的提问来源于stack exchange,提问作者user14047881

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.28 14:15:04