You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React中access token过期刷新、无活动跳转登录页方案咨询

React + DRF 架构下Token自动刷新与无操作登出实现方案

活跃状态下Access Token自动刷新实现

核心逻辑基于axios拦截器统一处理,避免每个业务请求重复写鉴权逻辑,同时解决并发请求触发重复刷新的问题:

  • 登录成功后,除access token外,需同时将refresh token、access token的过期时间戳存入localStorage。过期时间可以直接从JWT的payload中解析exp字段获取,无需后端额外返回。
  • 配置axios请求拦截器:所有业务请求发起前,先检查access token是否距离过期不足30秒(提前刷新阈值,可自行调整),若未过期则直接在请求头携带Authorization: Bearer ${accessToken}正常发送。
  • 配置axios响应拦截器捕获"access deny"(对应401状态码)错误,触发刷新逻辑时加全局锁防止重复请求:
    • 维护isRefreshing布尔标记和pendingRequests请求队列,第一个触发401的请求拿到锁后发起refresh请求,其余并发的过期请求全部加入队列等待
    • refresh请求调用DRF提供的token刷新接口(如simplejwt默认的/api/token/refresh/),请求体携带本地存储的refresh token,拿到新token后更新本地存储的凭证信息
    • 新token获取成功后,依次执行队列中挂起的请求回调,用新token重发所有挂起请求,再清空队列、释放锁
    • 如果refresh请求本身失败(refresh token过期、被拉黑等),直接清空本地所有凭证,跳转至登录页
      核心实现代码参考:
import axios from 'axios'
const request = axios.create({ baseURL: '/api' })
let isRefreshing = false
let pendingRequests = []

// 解析JWT过期时间
function getTokenExpire(token) {
  return JSON.parse(atob(token.split('.')[1])).exp * 1000
}

// 统一刷新token逻辑
async function refreshToken() {
  const refresh = localStorage.getItem('refreshToken')
  const { data } = await axios.post('/api/token/refresh/', { refresh })
  localStorage.setItem('accessToken', data.access)
  if (data.refresh) localStorage.setItem('refreshToken', data.refresh) // 适配refresh轮换配置
  const expire = getTokenExpire(data.access)
  localStorage.setItem('accessTokenExpire', expire)
  return data.access
}

// 请求拦截器:提前检查token状态
request.interceptors.request.use(async config => {
  const accessToken = localStorage.getItem('accessToken')
  if (!accessToken) return config
  const expire = Number(localStorage.getItem('accessTokenExpire'))
  // 跳过刷新接口本身的检查
  if (config.url.includes('/token/refresh/')) return config

  // 即将过期,提前触发刷新
  if (Date.now() > expire - 30 * 1000) {
    if (!isRefreshing) {
      isRefreshing = true
      try {
        const newToken = await refreshToken()
        config.headers.Authorization = `Bearer ${newToken}`
        return config
      } catch (err) {
        localStorage.clear()
        window.location.href = '/login'
        return Promise.reject(err)
      } finally {
        isRefreshing = false
      }
    } else {
      // 正在刷新,加入队列等待
      return new Promise(resolve => {
        pendingRequests.push(token => {
          config.headers.Authorization = `Bearer ${token}`
          resolve(config)
        })
      })
    }
  }
  config.headers.Authorization = `Bearer ${accessToken}`
  return config
})

// 响应拦截器:兜底处理漏网的401错误
request.interceptors.response.use(
  res => res.data,
  async err => {
    const originalReq = err.config
    if (err.response?.data?.detail === 'access deny' && !originalReq._retry) {
      originalReq._retry = true
      if (!isRefreshing) {
        isRefreshing = true
        try {
          const newToken = await refreshToken()
          // 重发所有挂起请求
          pendingRequests.forEach(cb => cb(newToken))
          pendingRequests = []
          originalReq.headers.Authorization = `Bearer ${newToken}`
          return request(originalReq)
        } catch (refreshErr) {
          localStorage.clear()
          window.location.href = '/login'
          return Promise.reject(refreshErr)
        } finally {
          isRefreshing = false
        }
      } else {
        return new Promise(resolve => {
          pendingRequests.push(token => {
            originalReq.headers.Authorization = `Bearer ${token}`
            resolve(request(originalReq))
          })
        })
      }
    }
    return Promise.reject(err)
  }
)

export default request

5分钟无操作自动登出实现

不推荐使用定时发后台探测请求的方案:一是平白增加后端无效请求压力,二是探测请求属于后台自动触发行为,会干扰无操作状态判定,容易出现用户早已离开页面、但探测请求持续续期token的bug。
前端完全可以独立完成活跃状态判定,实现逻辑如下:

  • 维护内存级变量lastActiveTime记录用户最后一次主动操作的时间戳,无需持久化存储
  • 监听window上的用户主动操作事件:click、keydown、mousemove、scroll、touchstart,事件触发时做防抖处理,更新lastActiveTime为当前时间
  • 启动间隔1分钟的轮询定时器,每次执行时计算当前时间与lastActiveTime的差值,差值超过5分钟则执行登出逻辑:清空本地凭证、移除事件监听、清除定时器、跳转登录页
  • 注意联动规则:后台自动触发的token刷新请求不能更新lastActiveTime,避免无操作状态被后台逻辑重置
    核心实现代码参考:
let lastActiveTime = Date.now()
const INACTIVE_DURATION = 5 * 60 * 1000 // 5分钟超时
const CHECK_GAP = 60 * 1000 // 1分钟检查一次
let checkTimer = null

// 防抖工具
function debounce(fn, delay) {
  let timer = null
  return (...args) => {
    clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, args), delay)
  }
}

// 更新活跃时间
const updateActive = debounce(() => {
  lastActiveTime = Date.now()
}, 1000)

// 绑定全局事件
const activeEvents = ['click', 'keydown', 'mousemove', 'scroll', 'touchstart']
activeEvents.forEach(evt => {
  window.addEventListener(evt, updateActive, { passive: true })
})

// 启动检查
checkTimer = setInterval(() => {
  if (Date.now() - lastActiveTime >= INACTIVE_DURATION) {
    // 执行登出
    localStorage.clear()
    activeEvents.forEach(evt => window.removeEventListener(evt, updateActive))
    clearInterval(checkTimer)
    window.location.href = '/login'
  }
}, CHECK_GAP)

补充注意事项

  • 如果项目使用React Router,可以将硬跳转window.location.href替换为路由的navigate方法,把登出逻辑抽成全局可调用的工具函数即可
  • 若对安全性要求更高,建议将refresh token存储在HttpOnly、Secure属性的Cookie中,避免XSS攻击窃取凭证
  • DRF端如果使用simplejwt,建议开启ROTATE_REFRESH_TOKENS = True和BLACKLIST_AFTER_ROTATION = True配置,每次刷新时轮换refresh token并将旧token加入黑名单,降低凭证泄露风险

内容的提问来源于stack exchange,提问作者Denis Morejón

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 02:24:34