React路由中NestJS Token验证中间件无法重定向至登录页的问题
问题分析与解决方案
核心问题拆解
- 重定向失效:React是单页应用(SPA),后端
res.redirect()仅对浏览器直接发起的页面请求有效。对于React Query等前端HTTP库发起的AJAX请求,浏览器会自动跟随重定向获取/login页面的HTML,但不会更新前端路由,最终导致页面空白。 - 错误未被捕获:中间件未针对API请求返回标准的错误响应,或者未确保错误状态码正确传递,导致React Query的
onError无法识别并处理异常。
修复方案
1. 改造NestJS中间件:区分页面请求与API请求
在中间件中通过请求路径、请求头判断请求类型,分别处理重定向和错误响应:
@Injectable() export class TokenValidationMiddleware implements NestMiddleware { async use(req: Request, res: Response, next: NextFunction) { const currentToken = req.cookies[config.auth.authTokenCookieName] ?? null; // 无Token的情况 if (!currentToken) { res.clearCookie(config.auth.authTokenCookieName, getCookieOptions()); const isApiRequest = req.path.startsWith('/api') || req.headers.accept?.includes('application/json'); if (isApiRequest) { // API请求返回401未授权,供前端捕获 res.status(401).json({ message: '未授权,请重新登录' }); } else { // 页面请求直接重定向 res.redirect('/login'); } return; // 终止后续逻辑执行 } try { const { expireTime, sessionId, username } = decryptToken(currentToken); const currentTime = new Date().getTime(); // Token过期的情况 if (expireTime < currentTime) { const sessionData = await findOne(sessionId); // 会话不存在或无操作记录 if (!sessionData?.lastActionTime) { res.clearCookie(config.auth.authTokenCookieName, getCookieOptions()); const isApiRequest = req.path.startsWith('/api') || req.headers.accept?.includes('application/json'); if (isApiRequest) { res.status(401).json({ message: '会话已过期,请重新登录' }); } else { res.redirect('/login'); } return; } const lastActionTime = sessionData.lastActionTime; const tokenRenewalMaxDuration = +config.auth.tokenRenewalMaxDuration; // 符合Token续期条件 if (currentTime - lastActionTime <= tokenRenewalMaxDuration * 1000) { const { token, tokenExpireTime } = generateAuthToken(decryptToken(currentToken)); res.cookie(config.auth.authTokenCookieName, token, getCookieOptions()); console.log(`TokenValidationMiddleware.use() - [${username}] - Token Renewed.`); // 异步方法必须加await,确保会话更新完成再放行 await updateExpireTime(sessionId, +tokenExpireTime, token); next(); } else { // 超出续期阈值,强制登录 console.log(`TokenValidationMiddleware.use() - [${username}] - User Token has Expired. Taking user to login.`); res.clearCookie(config.auth.authTokenCookieName, getCookieOptions()); const isApiRequest = req.path.startsWith('/api') || req.headers.accept?.includes('application/json'); if (isApiRequest) { res.status(401).json({ message: '会话已过期,请重新登录' }); } else { res.redirect('/login'); } } } else { // Token有效,放行请求 next(); } } catch (error) { // 处理Token解密、数据库操作等异常 console.error(`TokenValidationMiddleware.use() - 验证失败: ${error.message}`); res.clearCookie(config.auth.authTokenCookieName, getCookieOptions()); const isApiRequest = req.path.startsWith('/api') || req.headers.accept?.includes('application/json'); if (isApiRequest) { res.status(401).json({ message: 'Token验证失败,请重新登录' }); } else { res.redirect(`/login?error=${encodeURIComponent(error.message)}`); } } } }
2. 前端React Query配置全局错误捕获
在QueryClient中通过缓存的onError钩子捕获401错误,触发前端路由跳转:
import { QueryClient } from '@tanstack/react-query'; const queryClient = new QueryClient({ queryCache: { onError: (error) => { if (error.response?.status === 401) { // 跳转到登录页 window.location.href = '/login'; } }, }, mutationCache: { onError: (error) => { if (error.response?.status === 401) { window.location.href = '/login'; } }, }, });
3. 关键细节注意
- 所有分支必须通过
return终止逻辑,避免后续代码意外执行; - 异步方法(如
updateExpireTime)必须加await,确保操作完成再放行请求; - 清除Cookie时要传入与设置Cookie一致的选项(如domain、path),否则可能清除失败;
- 错误信息需用
encodeURIComponent编码,避免URL参数乱码。
内容的提问来源于stack exchange,提问作者Rafael Pérez
相关产品推荐
相关产品推荐

