基于Express/React/Mongoose的JWT登录系统Cookie刷新异常求助
问题分析与解决方案
核心问题
你遇到的问题本质是并发请求导致的Token冲突:React端重复发送2个携带旧Refresh Token的请求,第一个请求处理时会将旧Token从用户的refreshToken数组中移除并生成新Token;第二个请求到达时,旧Token已被服务端标记无效,导致查找用户失败,最终返回403并清空Cookie,引发后续认证异常。
同时原服务端代码存在两个关键逻辑缺陷:
- 过早清除Cookie,导致第二个请求在新Cookie生效前仍携带旧Token
- 异步校验逻辑存在重复响应风险,且未处理并发修改场景
修复方案
1. 前端:解决重复请求问题
首先要从根源阻止重复请求,避免并发冲突:
- 检查
useEffect等触发请求的逻辑,添加正确的依赖项,防止重复渲染触发请求 - 实现请求锁机制,确保同一时刻只有一个Refresh请求在处理:
let isRefreshing = false; async function triggerRefresh() { if (isRefreshing) return; isRefreshing = true; try { const res = await fetch('/api/refresh', { credentials: 'include' }); // 处理响应逻辑 } catch (err) { // 错误处理 } finally { isRefreshing = false; } }
2. 服务端:重构Refresh路由逻辑
调整Token校验、用户查找和Cookie设置的顺序,避免并发冲突:
const refresh = asyncHandler(async (req, res) => { const cookies = req.cookies; if (!cookies?.jwt) return res.status(401).json({ message: "Unauthorized user" }); const refreshToken = cookies.jwt; let user; try { // 先解码Token获取用户名,避免并发修改导致用户查找失败 const decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET); user = await Users.findOne({ username: decoded.username }).exec(); if (!user) return res.sendStatus(403); // 检查当前请求的Token是否仍在用户的有效Token列表中 const isTokenValid = user.refreshToken.includes(refreshToken); if (!isTokenValid) { // Token已被失效,清空用户所有Refresh Token user.refreshToken = []; await user.save(); return res.sendStatus(403); } // 生成新的Access和Refresh Token const accessToken = generateToken({ "UserInfo": { "username": user.username, "role": user.role } }, "access"); const newRefreshToken = generateToken({ username: user.username }, "refresh"); // 更新用户Token列表:移除旧Token,添加新Token user.refreshToken = user.refreshToken.filter(rt => rt !== refreshToken); user.refreshToken.push(newRefreshToken); await user.save(); // 设置新Cookie(自动覆盖旧Cookie,无需手动清除) res.cookie('jwt', newRefreshToken, { httpOnly: true, secure: true, sameSite: 'None', maxAge: 24 * 60 * 60 * 1000 }); res.json({ role: user.role, accessToken }); } catch (err) { if (err.name === 'JsonWebTokenError') { // Token无效时,清空该用户所有Refresh Token if (user) { user.refreshToken = []; await user.save(); } return res.sendStatus(403); } res.sendStatus(500); } });
关键优化点说明
- 延迟Cookie操作:不再提前清除Cookie,而是用新Cookie直接覆盖旧Cookie,确保前端只有在请求成功后才会拿到有效Token
- 优先解码Token:通过JWT解码获取用户名查找用户,避免因并发修改导致的用户查找失败
- Token有效性校验:检查请求携带的Token是否仍在用户的有效列表中,处理并发场景下的Token失效问题
- 统一错误处理:整合异步校验逻辑,避免重复响应的问题
内容的提问来源于stack exchange,提问作者dislonely
相关产品推荐
相关产品推荐

