一、为什么需要“无感刷新”令牌?

1.1 传统方案的痛点

你有没有过这样的经历?刷着视频网站的长视频,看到一半突然弹出“登录过期,请重新登录”的弹窗;或者用外卖APP查看未完成订单,准备叫骑手联系商家时,页面直接跳转到登录页。这些情况都是因为系统没有处理好令牌的过期问题——传统方案是等请求失败后才告诉你“令牌没了”,打断了你的操作流程,极大影响了使用体验。

1.2 无感刷新的核心解决思路

其实就是给系统加了一个“提前换临时卡”的功能,不用等你发现卡过期,系统自己悄悄换好,用户完全感知不到,操作可以继续。这个功能的核心是让系统自动管理令牌的生命周期,不需要用户手动介入。

二、核心方案:短期令牌+刷新令牌

2.1 两个令牌的分工

用生活化的例子来说:短期令牌就相当于商场的临时门禁卡,有效期很短,比如10分钟,每次进电梯、进店铺都要刷一次,过期就不能用了;刷新令牌相当于你家的钥匙,是用来换临时门禁卡的,有效期长一点,比如7天,平时放在抽屉里,不能随便丢,只有在临时卡快过期的时候才能拿出来用。这样的分工既保证了安全,又能让系统自动续卡。

2.2 客户端需要存什么

短期令牌因为有效期短,最好存在内存里(比如闭包变量或者内存对象),页面关闭后就自动消失,就算被偷也用不了多久;刷新令牌要存在安全的地方,比如后端设置的HttpOnly Cookie,前端JavaScript拿不到,避免被XSS攻击偷取,也可以存在加密的LocalStorage里,不过要注意加密方式,不能明文存储。

三、前端完整实现(JavaScript技术栈)

3.1 技术栈说明

本次实现使用JavaScript + axios,这是前端开发中最通用的组合,适配Web端,也兼容混合APP里的Webview,不需要额外的框架,只要引入axios即可,方便不同基础的开发者快速上手。

// 技术栈:JavaScript + axios(前端通用HTTP请求库,适配Web与混合APP Webview)
import axios from 'axios';

// 1. 配置全局axios实例,统一管理请求基础设置
const http = axios.create({
  baseURL: 'https://你的后端API地址', // 替换为实际的后端接口地址
  timeout: 10000, // 请求超时时间设为10秒,避免长时间等待
});

// 2. 定义令牌相关的常量与存储键,避免硬编码,方便后续修改
const ACCESS_TOKEN_KEY = 'access_token'; // 短期令牌存储键
const REFRESH_TOKEN_KEY = 'refresh_token'; // 刷新令牌存储键
const TOKEN_REFRESH_BUFFER = 60 * 1000; // 提前60秒刷新令牌,避免刚过期就请求
let isRefreshing = false; // 标记是否正在刷新令牌,避免并发请求重复刷新

// 3. 封装操作令牌的基础函数,统一入口方便维护
function getAccessToken() {
  return localStorage.getItem(ACCESS_TOKEN_KEY);
}

function getRefreshToken() {
  return localStorage.getItem(REFRESH_TOKEN_KEY);
}

// 简化判断令牌是否过期(实际项目需解析JWT的exp字段,此处为示例简化)
function isTokenNearExpired() {
  const token = getAccessToken();
  return !token; // 无令牌视为即将过期
}

// 4. 封装刷新令牌的核心请求函数,处理令牌替换逻辑
async function refreshAuthToken() {
  const refreshToken = getRefreshToken();
  if (!refreshToken) throw new Error('无有效刷新令牌,请重新登录');

  try {
    // 调用后端刷新令牌接口,传入当前刷新令牌
    const response = await http.post('/api/auth/refresh', { refreshToken });
    // 更新本地存储的新令牌
    localStorage.setItem(ACCESS_TOKEN_KEY, response.data.accessToken);
    localStorage.setItem(REFRESH_TOKEN_KEY, response.data.refreshToken);
    return response.data.accessToken;
  } catch (error) {
    // 刷新失败说明令牌已失效,强制跳转到登录页
    localStorage.removeItem(ACCESS_TOKEN_KEY);
    localStorage.removeItem(REFRESH_TOKEN_KEY);
    window.location.href = '/login';
    throw error;
  }
}

// 5. 请求拦截器:给所有请求自动添加令牌,提前触发过期刷新
http.interceptors.request.use(async (config) => {
  let token = getAccessToken();
  // 如果令牌即将过期,且没有正在刷新的请求,触发刷新
  if (isTokenNearExpired() && !isRefreshing) {
    isRefreshing = true;
    try {
      token = await refreshAuthToken();
    } finally {
      isRefreshing = false;
    }
  }
  // 将令牌添加到请求头,符合后端要求的Authorization格式
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// 6. 响应拦截器:处理令牌过期的401错误,自动重试原请求
http.interceptors.response.use(
  (response) => response, // 正常响应直接返回
  async (error) => {
    const originalRequest = error.config;
    // 仅处理401错误,且不是刷新接口本身的请求,避免死循环
    if (error.response?.status === 401 && !originalRequest._retry && originalRequest.url !== '/api/auth/refresh') {
      originalRequest._retry = true;
      if (!isRefreshing) {
        isRefreshing = true;
        try {
          const newToken = await refreshAuthToken();
          // 用新令牌重新发起原来的请求
          originalRequest.headers.Authorization = `Bearer ${newToken}`;
          return http(originalRequest);
        } finally {
          isRefreshing = false;
        }
      }
    }
    // 其他错误直接抛出,由业务层处理
    return Promise.reject(error);
  }
);

export default http;

3.2 代码逻辑拆解

刚才的代码分为6个核心模块,每个模块都对应实际开发中的需求:首先是配置axios实例统一管理基础请求设置,避免每个请求重复写地址和超时;然后是定义常量和状态变量,方便后续修改和追踪刷新状态;接着封装了令牌的基础操作函数,统一入口避免逻辑分散;然后是核心的刷新令牌函数,处理后端接口调用和本地存储更新;请求拦截器负责提前检查令牌状态,自动触发刷新;响应拦截器负责处理请求失败的情况,自动重试。其中最关键的isRefreshing变量,是为了避免多个请求同时触发刷新,给后端造成不必要的压力,保证只有一个刷新请求在运行。

四、应用场景

4.1 Web端场景

电商的商品详情页、资讯平台的文章页,用户滚动页面、翻页时,系统会自动请求接口获取更多内容,如果用了无感刷新,用户不会感觉到令牌的变化,操作全程流畅。比如你在京东上对比三款手机,翻页看更多评价,不会因为令牌过期停在半路上,影响选购体验。

4.2 移动端场景

混合APP里的Webview(比如外卖APP里的店铺详情页、打车APP里的行程页面),用户使用APP时,页面可能会持续请求后台数据,无感刷新能让用户在打开Webview后的很长时间里不用手动登录,提升APP的体验评分,减少用户因登录问题退出APP的概率。

4.3 内部管理后台

企业的内部OA系统、CRM系统,员工操作表单、审批流程时,突然的登出会打断工作进度,无感刷新能保证员工连续操作,不用频繁登录,提升办公效率,减少因重复登录导致的厌烦情绪。

五、技术优缺点

5.1 优点

第一,用户体验好,全程无感知,不会被弹窗或跳转打断;第二,安全性高,短期令牌有效期短,就算泄露,10分钟就失效,降低了被攻击的风险;第三,兼容性强,一套逻辑适配Web、混合APP的Webview,不需要为不同端写不同代码,减少重复开发。

5.2 缺点

第一,实现有一定复杂度,需要处理并发刷新、错误重试、令牌存储等多个细节,对开发人员的逻辑能力有一定要求;第二,依赖后端接口,后端必须支持刷新令牌的API,并且生成带过期时间的令牌(比如JWT),如果后端没有适配,这个方案无法落地;第三,对安全意识要求高,令牌存储的安全方式需要严格遵守,不能随便存在容易被窃取的地方。

六、注意事项

6.1 令牌存储安全

刷新令牌必须存在HttpOnly Cookie,这样前端JS无法读取,避免XSS攻击窃取;短期令牌可以存在内存中,或者经过加密的LocalStorage,不要明文存储在容易被读取的地方,比如URL参数或者本地缓存的明文字段,防止令牌泄露。

6.2 并发请求处理

必须加isRefreshing的标记,避免多个请求同时触发刷新,比如10个请求在同一时间都发现令牌过期,只会让一个请求去刷新,其他请求等新令牌回来再执行,这样不会给后端造成压力,也避免重复请求刷新接口导致的性能浪费。

6.3 过期时间设置

短期令牌的有效期一般设为10-30分钟,刷新令牌设为7天,提前刷新的缓冲时间设为60秒左右,给后端留足够的处理时间,不要太接近过期时间,避免请求刚发出令牌就过期导致失败,影响用户体验。

6.4 错误处理

如果刷新令牌的请求失败,必须立即清除本地存储的所有令牌,跳转到登录页,不能让用户卡在错误页面,也不能继续使用过期令牌,不然会造成安全风险,被恶意用户利用。

6.5 移动端适配

混合APP的Webview要注意Cookie的同步,APP进入后台或者退出时,要清除相关的Token缓存,避免用户下次打开时残留的旧令牌导致的问题,还要注意Webview的Cookie策略,比如跨域的Cookie是否被允许,避免出现请求失败的情况。

七、总结

无感刷新访问令牌的方案,是现在前端开发中解决用户登录体验与安全性平衡的标准方案,通过短期令牌和刷新令牌的分工,让用户不用手动处理过期问题,全程流畅操作,同时通过安全的存储方式和错误处理,保证了系统的安全性。按照刚才的完整实现示例,开发者可以快速在自己的项目中落地,不管是Web端还是移动端混合APP,都能提升产品的用户体验,减少因登录问题导致的用户流失,真正实现产品交互的流畅性与安全性的双重保障。