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
相关产品推荐
相关产品推荐

