双Token无感刷新:Vue3 + Axios 企业级完整实现

在前后端分离架构中,基于 Token 的 JWT 鉴权是目前最主流的身份认证方案。但传统单 Token 模式存在明显短板:令牌时效过短会导致用户频繁掉线,时效过长又会极大增加信息泄露风险,严重影响产品体验与系统安全性。双 Token 无感刷新机制是业界标准的最优解决方案,能够在用户无感知的前提下静默续期身份凭证,无需弹窗、无需跳转、不中断业务操作。本文将从原理剖析、前端工程化落地、后端 JWT 签发、并发问题优化、企业级安全规范五大维度,提供一套可直接上线的生产级完整方案。


一、为什么需要无感刷新

单 Token 模式无法同时兼顾「安全性」与「用户体验」:若令牌有效期较短,用户正常操作会被强制下线;若有效期过长,一旦令牌被盗,攻击者可长期冒用身份,存在极高安全漏洞。

无感刷新的核心价值:通过长短双令牌分离职责,在保证接口安全的前提下,自动静默续期登录状态,实现用户永久在线、全程无感知,彻底解决主动掉线问题。


二、双 Token 机制原理

双 Token 机制的核心设计思想是职责拆分、长短搭配,用短期令牌保障接口安全,用长期令牌负责身份续期,从架构层面解决安全与体验的冲突。两枚令牌各司其职、互不干扰:

  • Access Token(业务短期令牌) :有效期建议 15--60 分钟,专门用于业务接口鉴权。短时效设计可以大幅缩小令牌泄露的风险窗口,保障接口访问安全,是用户访问资源的核心凭证。
  • Refresh Token(刷新长期令牌) :有效期建议 7--30 天,仅用于刷新 Access Token,不参与任何业务请求。作为身份续期的唯一凭证,用于静默续期登录状态。

整套无感刷新拥有完整闭环逻辑,运行机制清晰、稳定性强,具体流程如下:

  1. 用户登录校验成功后,后端同步返回全新的 Access Token 与 Refresh Token
  2. 前端本地持久化双令牌,所有业务请求自动携带 Access Token 完成鉴权
  3. Access Token 过期失效,接口请求返回 401 未授权状态码
  4. 前端拦截 401 错误,自动调用刷新接口,携带 Refresh Token 申请新凭证
  5. 刷新成功:更新本地双令牌,自动重试失败的业务请求,用户操作全程无感知
  6. 刷新失败:判定长效凭证过期、登录态彻底失效,清空本地缓存,跳转登录页重新授权

三、前端完整实现(Vue3 + Axios)

1、登录逻辑:双令牌获取与本地持久化

javascript 复制代码
const login = async () => {
  const userInfo = { username: 'zs', password: '123' }
  const { data } = await userLogin(userInfo)

  // 存储双Token
  localStorage.setItem('access_token', data.access_token)
  localStorage.setItem('refresh_token', data.refresh_token)
  isLogin.value = true
}

2、请求拦截器:统一挂载鉴权头部

ini 复制代码
api.interceptors.request.use(config => {
  const token = localStorage.getItem('access_token')
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
})

3、响应拦截器:生产级并发防重刷 + 自动续期 + 请求重试

javascript 复制代码
// 核心全局变量:解决多接口并发401重复刷新BUG
// 刷新锁:控制同一时间仅执行一次token刷新
let isRefreshing = false
// 请求队列:存储刷新期间失败的所有业务请求
let requestQueue = []

api.interceptors.response.use(
  // 正常响应直接返回
  res => res,
  // 统一处理异常响应
  async error => {
    const { response, config } = error || {}
    const status = response?.status
    const isRefreshApi = config.url === '/refresh'

    // 非401错误、刷新接口自身报错,直接抛出异常,不做处理
    if (status !== 401 || isRefreshApi) {
      return Promise.reject(error)
    }

    // 场景一:正在刷新中,将当前失败请求推入队列,等待刷新完成重试
    if (isRefreshing) {
      return new Promise(resolve => {
        requestQueue.push(() => resolve(api(config)))
      })
    }

    // 场景二:无刷新进行中,开启锁,执行刷新逻辑
    isRefreshing = true

    try {
      // 调用刷新接口,更新双令牌
      await refreshToken()
      // 刷新成功:批量执行队列中所有等待请求
      requestQueue.forEach(cb => cb())
      // 清空队列,避免脏数据残留
      requestQueue = []
      // 重试当前失败的接口
      return api(config)
    } catch (e) {
      // 刷新失败:refresh_token 彻底失效,重置登录态
      localStorage.clear()
      requestQueue = []
      window.location.href = '/login'
      return Promise.reject(e)
    } finally {
      // 无论成功失败,最终释放刷新锁,允许下次刷新
      isRefreshing = false
    }
  }
)

4、刷新函数封装:统一更新本地令牌

javascript 复制代码
async function refreshToken() {
  const rt = localStorage.getItem('refresh_token')
  const { data } = await api.get('/refresh', { params: { token: rt } })

  localStorage.setItem('access_token', data.access_token)
  localStorage.setItem('refresh_token', data.refresh_token)
  return data
}

四、后端完整实现(Node.js + Express + JWT)

1、登录接口:签发长短双令牌

javascript 复制代码
app.post('/login', (req, res) => {
  const { username, password } = req.body
  const user = users.find(u => u.username === username && u.password === password)

  if (!user) return res.status(401).json({ message: '账号或密码错误' })

  // 生成短时效访问Token + 长时效刷新Token
  const access_token = generateToken(user, '1h')
  const refresh_token = generateToken(user, '7d')

  res.json({ access_token, refresh_token, userInfo: user })
})

2、令牌刷新接口:续期新凭证

javascript 复制代码
app.get('/refresh', (req, res) => {
  const { token } = req.query
  try {
    const userData = verifyToken(token)
    const access_token = generateToken(userData, '1h')
    const refresh_token = generateToken(userData, '7d')
    res.json({ access_token, refresh_token })
  } catch (err) {
    res.status(401).json({ message: '刷新令牌已失效' })
  }
})

3、JWT工具类:统一签发与校验

javascript 复制代码
const jwt = require('jsonwebtoken')
const secret = 'MY_SECRET_KEY'

function generateToken(user, expiresIn) {
  return jwt.sign({ username: user.username }, secret, { expiresIn })
}

function verifyToken(token) {
  return jwt.verify(token, secret)
}

module.exports = { generateToken, verifyToken }

五、整体业务闭环梳理

  1. 业务请求发起,请求拦截器自动携带有效 Access Token
  2. AccessToken 过期或无效,后端返回 401 未授权状态码
  3. 前端拦截 401 鉴权失败状态,静默阻断报错,自动调用令牌刷新接口
  4. 刷新接口请求成功,更新本地双令牌,自动重试失败的原始请求
  5. 刷新接口请求失败,判定登录态失效,清空本地凭证,跳转登录页重新授权

六、企业级安全规范与生产避坑指南

  • 合理配置 AccessToken 时效:推荐 15--60 分钟短期有效期,缩小令牌泄露风险窗口,保障接口鉴权安全
  • 严格管控 RefreshToken 时效:有效期控制在 7--30 天,禁止永久有效,防止长效令牌被盗用造成长期风险
  • 生产环境强制开启 HTTPS:全程加密传输,防止中间人抓包劫持令牌,保障传输链路安全
  • 优化 RefreshToken 存储方案:生产环境优先使用 HttpOnly + Secure Cookie 存储,规避 LocalStorage 带来的 XSS 窃取漏洞
  • 开启令牌轮换机制:每次刷新同步更新 RefreshToken,主动作废旧令牌,避免旧令牌被复用攻击
  • 刷新接口增加安全校验:后端需对刷新接口做限流、IP 绑定、设备标识校验,抵御恶意刷令牌攻击
  • 敏感业务二次鉴权:支付、权限配置、账号核心操作等接口,不可仅依赖 Token,需增加二次身份校验机制
  • 规范 JWT 载荷数据:Payload 仅存储用户ID、账号等非敏感标识,严禁存入密码、密钥、权限核心数据
  • 规范前端令牌存储:避免明文令牌长期暴露在可被JS读取的存储中,降低XSS窃取风险
  • 解决并发重复刷新致命BUG(生产核心优化) :多接口同时401会触发多次刷新,造成令牌覆盖、请求报错。本文采用「刷新锁 + 请求队列」机制,全局仅执行一次刷新,其余请求排队重试,彻底解决并发刷新问题,代码已完全落地可直接使用
  • 刷新失败强制清空登录态:刷新异常时彻底清除本地令牌缓存,杜绝脏数据导致的循环报错、无限重试问题

相关推荐
前端那点事1 小时前
Vue Token鉴权避坑指南|5步完整实现(从生成到失效全解析)
前端·vue.js
Momo__1 小时前
package.json 配置详解:依赖管理深度指南
前端
漫游的渔夫1 小时前
前端开发者做 Agent:模型说执行就执行?先加 3 道闸门再碰真实业务
前端·人工智能·typescript
前端那点事1 小时前
企业级Vue前端鉴权方案全解析|从Token到OAuth2.0,覆盖多端适配+权限管控
前端·vue.js
亲亲小宝宝鸭1 小时前
从Vben-Admin里面学习hooks
前端·vue.js
Mintopia1 小时前
MSW Mock Feature-First 方案
前端·架构
sin6031 小时前
Talk is cheap 之后:AI Agent 时代,程序员真正要交付什么?
前端
Ticnix1 小时前
手把手教你在 Next.js 中接入本地大模型,实现 ChatGPT 同款流式对话
前端·next.js
ZC跨境爬虫1 小时前
跟着 MDN 学 HTML day_18:(HTML 表格进阶特性与无障碍——从标题结构到屏幕阅读器适配)
前端·笔记·ui·html·音视频