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

Remix全局Token刷新处理:避免并行Loader中的并发请求

解决Remix+Express并发Token刷新问题的方案

针对Remix并行loader导致的多路由同时触发Token刷新的问题,核心思路是全局串行化同一用户的刷新请求,让所有并行请求复用同一个刷新结果,避免重复开销。以下是几种实用的实现方案:

1. Express后端全局刷新锁(单实例场景)

在Express中间件中维护一个全局的Promise缓存,同一用户的刷新请求会复用同一个正在进行的刷新Promise,避免重复发起请求。

// Express 全局变量,按用户ID存储正在进行的刷新Promise
const refreshPromises = new Map();

async function refreshTokenMiddleware(req, res, next) {
  const { accessToken, refreshToken, userId } = req.session;

  if (!accessToken || isTokenExpired(accessToken)) {
    // 检查该用户是否已有刷新请求在进行
    if (refreshPromises.has(userId)) {
      try {
        const newTokens = await refreshPromises.get(userId);
        req.session.accessToken = newTokens.accessToken;
        req.session.refreshToken = newTokens.refreshToken;
        return next();
      } catch (err) {
        return res.redirect('/login');
      }
    }

    // 发起新的刷新请求并缓存Promise
    const refreshTask = fetch('http://your-auth-service/refresh', {
      method: 'POST',
      body: JSON.stringify({ refreshToken }),
      headers: { 'Content-Type': 'application/json' }
    })
    .then(res => {
      if (!res.ok) throw new Error('Refresh failed');
      return res.json();
    })
    .then(newTokens => {
      refreshPromises.delete(userId);
      return newTokens;
    })
    .catch(err => {
      refreshPromises.delete(userId);
      throw err;
    });

    refreshPromises.set(userId, refreshTask);

    try {
      const newTokens = await refreshTask;
      req.session.accessToken = newTokens.accessToken;
      req.session.refreshToken = newTokens.refreshToken;
      next();
    } catch (err) {
      req.session.destroy();
      res.redirect('/login');
    }
  } else {
    next();
  }
}

// 将中间件应用到所有路由
app.use(refreshTokenMiddleware);

2. Remix Loader全局复用策略

将Token刷新逻辑抽离为全局服务,在各个loader中调用该服务,确保同一用户的刷新请求串行化。

// ~/services/tokenService.js
const refreshPromises = new Map();

export async function refreshUserToken(userId, refreshToken) {
  if (refreshPromises.has(userId)) {
    return refreshPromises.get(userId);
  }

  const task = fetch('http://your-auth-service/refresh', {
    method: 'POST',
    body: JSON.stringify({ refreshToken }),
    headers: { 'Content-Type': 'application/json' }
  })
  .then(res => {
    if (!res.ok) throw new Error('Refresh failed');
    return res.json();
  })
  .then(newTokens => {
    refreshPromises.delete(userId);
    return newTokens;
  })
  .catch(err => {
    refreshPromises.delete(userId);
    throw err;
  });

  refreshPromises.set(userId, task);
  return task;
}

在Remix loader中使用:

// ~/routes/dashboard.js
import { json, redirect } from '@remix-run/node';
import { getSession, commitSession, destroySession } from '~/services/session';
import { refreshUserToken } from '~/services/tokenService';
import { isTokenExpired } from '~/utils/token';

export async function loader({ request }) {
  const session = await getSession(request.headers.get('Cookie'));
  const accessToken = session.get('accessToken');
  const refreshToken = session.get('refreshToken');
  const userId = session.get('userId');

  if (!accessToken || isTokenExpired(accessToken)) {
    try {
      const newTokens = await refreshUserToken(userId, refreshToken);
      session.set('accessToken', newTokens.accessToken);
      session.set('refreshToken', newTokens.refreshToken);
    } catch (err) {
      return redirect('/login', {
        headers: { 'Set-Cookie': await destroySession(session) }
      });
    }
  }

  // 正常业务请求逻辑
  const dashboardData = await fetch('http://your-api/dashboard', {
    headers: { Authorization: `Bearer ${session.get('accessToken')}` }
  }).then(res => res.json());

  return json(dashboardData, {
    headers: { 'Set-Cookie': await commitSession(session) }
  });
}

3. 分布式锁(多实例部署场景)

如果Express是多实例集群部署,全局内存变量无法共享,需要用Redis等分布式锁来确保同一用户只有一个实例发起刷新请求。

const redis = require('redis');
const client = redis.createClient({ url: 'redis://your-redis-server' });
const lock = require('redis-lock')(client);

async function refreshTokenMiddleware(req, res, next) {
  const { accessToken, refreshToken, userId } = req.session;

  if (!accessToken || isTokenExpired(accessToken)) {
    // 为当前用户创建分布式锁,超时5秒
    const unlock = await lock(`refresh:${userId}`, 5000);

    try {
      // 双重检查:等待锁期间可能已完成刷新
      if (!isTokenExpired(req.session.accessToken)) {
        return next();
      }

      const newTokens = await fetch('http://your-auth-service/refresh', {
        method: 'POST',
        body: JSON.stringify({ refreshToken }),
        headers: { 'Content-Type': 'application/json' }
      }).then(res => res.json());

      req.session.accessToken = newTokens.accessToken;
      req.session.refreshToken = newTokens.refreshToken;
      next();
    } catch (err) {
      req.session.destroy();
      res.redirect('/login');
    } finally {
      // 释放锁
      unlock();
    }
  } else {
    next();
  }
}

关键注意事项

  • 刷新失败处理:如果Refresh Token也过期,要及时销毁session并跳转到登录页。
  • 锁超时设置:合理设置锁的超时时间,避免死锁(建议略长于刷新请求的预期耗时)。
  • 用户维度隔离:所有锁和Promise缓存都要绑定用户ID,避免不同用户的刷新请求互相阻塞。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 04:13:18