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

NestJS:自定义JWT Guard抛异常至Filter,返回自定义UnauthorizedException

问题描述

我编写了一个自定义JWT Guard,当检测到请求Cookie中缺少access token时会抛出错误。当前返回的是ForbiddenException,我希望向React客户端返回带有自定义消息的UnauthorizedException。请问从Guard抛出异常并在Filter中捕获的正确方式是什么?是否有更优实现方案?

相关代码及日志

JwtAuthGuard.ts

const cookieExtractor = (req: Request) => {
  if (req && req.cookies) {
    return req.cookies['jwt'];
  }
};

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  private logger = new Logger(JwtAuthGuard.name);

  constructor() {
    super();
  }

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();

    try {
      const accessToken = ExtractJwt.fromExtractors([cookieExtractor])(request);

      if (!accessToken)
        throw new UnauthorizedException('Access token is not set');

      return this.activate(context);
    } catch (err) {
      this.logger.error((err as Error).message);
      return false;
    }
  }

  async activate(context: ExecutionContext): Promise<boolean> {
    return super.canActivate(context) as Promise<boolean>;
  }
}

AuthExceptionFilter.ts

@Catch(HttpException)
export class AuthExceptionFilter extends BaseExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    console.log('exception', exception);

    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
    });

    super.catch(exception, host);
  }
}

Controller.ts

@Get('/status')
  @UseGuards(JwtAuthGuard)
  @UseFilters(AuthExceptionFilter)
  async status(@Req() req: Request) {
    return true;
  }

控制台异常日志

[Nest] 65933  - 02/24/2023, 10:44:38 AM   ERROR [JwtAuthGuard] Access token is not set
exception ForbiddenException: Forbidden resource
    at canActivateFn (/test-project/node_modules/@nestjs/core/router/router-execution-context.js:136:23)
    at processTicksAndRejections (node:internal/process/task_queues:95:5)
    at /test-project/node_modules/@nestjs/core/router/router-execution-context.js:42:31
    at /test-project/node_modules/@nestjs/core/router/router-proxy.js:9:17 {
  response: {
    statusCode: 403,
    message: 'Forbidden resource',
    error: 'Forbidden'
  },
  status: 403,
  options: {}
}
问题原因

当前Guard的canActivate方法中捕获异常后返回false,Nest框架会自动将返回false的Guard校验结果转换为ForbiddenException,导致你自定义抛出的UnauthorizedException被覆盖,最终Filter接收到的是默认的403异常。

修复方案

1. 修正Guard的异常处理逻辑

不要在canActivate中返回false,而是直接抛出异常,或在catch块中重新抛出异常:

async canActivate(context: ExecutionContext): Promise<boolean> {
  const request = context.switchToHttp().getRequest();

  try {
    const accessToken = ExtractJwt.fromExtractors([cookieExtractor])(request);

    if (!accessToken) {
      throw new UnauthorizedException('Access token is not set');
    }

    return super.canActivate(context) as Promise<boolean>;
  } catch (err) {
    this.logger.error((err as Error).message);
    // 重新抛出异常,不要返回false
    throw err;
  }
}

若不需要日志,可简化为:

async canActivate(context: ExecutionContext): Promise<boolean> {
  const request = context.switchToHttp().getRequest();
  const accessToken = ExtractJwt.fromExtractors([cookieExtractor])(request);

  if (!accessToken) {
    throw new UnauthorizedException('Access token is not set');
  }
  
  return super.canActivate(context) as Promise<boolean>;
}

2. 调整Exception Filter,返回自定义消息

修改Filter,将异常的自定义消息加入响应体,同时避免重复发送响应(去掉super.catch):

@Catch(HttpException)
export class AuthExceptionFilter extends BaseExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();
    const exceptionResponse = exception.getResponse();

    // 提取异常消息,兼容字符串和对象格式
    const message = typeof exceptionResponse === 'string'
      ? exceptionResponse
      : (exceptionResponse as { message: string | string[] }).message;

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: message // 加入自定义消息
    });
  }
}
更优实现方案

利用Nest JWT模块原生能力,无需手动判断token是否存在:

1. 在Guard中配置Token提取器

直接在Guard构造函数中通过super配置JWT提取器,让框架自动处理token缺失逻辑:

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  private logger = new Logger(JwtAuthGuard.name);

  constructor() {
    super({
      // 指定从Cookie提取token
      jwtFromRequest: ExtractJwt.fromExtractors([cookieExtractor]),
    });
  }

  // 重写handleRequest方法,自定义异常消息
  handleRequest(err: any, user: any, info: any) {
    if (err || !user) {
      // 根据JWT模块的错误信息返回自定义提示
      throw err || new UnauthorizedException(info?.message || 'Access token is missing or invalid');
    }
    return user;
  }
}

2. 移除手动token校验

此时canActivate方法可完全复用父类逻辑,无需自己实现,进一步简化代码。框架会自动检测token是否存在,缺失时抛出UnauthorizedException,配合自定义Filter即可返回带自定义消息的响应。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 01:48:21