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

React路由中NestJS Token验证中间件无法重定向至登录页的问题

问题分析与解决方案

核心问题拆解

  1. 重定向失效:React是单页应用(SPA),后端res.redirect()仅对浏览器直接发起的页面请求有效。对于React Query等前端HTTP库发起的AJAX请求,浏览器会自动跟随重定向获取/login页面的HTML,但不会更新前端路由,最终导致页面空白。
  2. 错误未被捕获:中间件未针对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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 00:42:51