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

NestJs如何实现公开路由下条件排除实体属性的装饰器

实现方案

基于class-transformer序列化逻辑+Nest元数据能力即可实现,不需要改动现有JWT认证、@Public()装饰器的原有逻辑,分三步落地:

    1. 实现@ExcludeIfPublic()属性装饰器
      这个装饰器的作用是给需要条件排除的字段打上元数据标记,方便后续序列化阶段识别:
import 'reflect-metadata';

export const EXCLUDE_IF_PUBLIC_KEY = 'exclude_if_public';
export const ExcludeIfPublic = () => {
  return (target: object, propertyKey: string) => {
    Reflect.defineMetadata(EXCLUDE_IF_PUBLIC_KEY, true, target, propertyKey);
  };
};
    1. 自定义序列化拦截器,替换默认的ClassSerializerInterceptor
      默认的序列化拦截器无法感知当前路由是否为公开路由,因此需要扩展拦截逻辑,在公开路由场景下额外排除标记字段:
import { Injectable, ExecutionContext, CallHandler, ClassSerializerInterceptor, ClassTransformOptions } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { instanceToPlain } from 'class-transformer';
import { map, Observable } from 'rxjs';
import { IS_PUBLIC_KEY } from './public.decorator'; // 导入你已定义的公开路由元数据key
import { EXCLUDE_IF_PUBLIC_KEY } from './exclude-if-public.decorator';

@Injectable()
export class ConditionalExcludeInterceptor extends ClassSerializerInterceptor {
  constructor(reflector: Reflector) {
    super(reflector);
  }

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    // 读取当前路由的公开标记
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    const baseOptions = this.getOptions(this.getContextOptions(context));

    return next.handle().pipe(
      map((res) => {
        // 非公开路由直接走默认序列化逻辑,所有字段正常返回
        if (!isPublic) return this.serialize(res, baseOptions);
        // 公开路由额外处理条件排除字段
        return this.processPublicResponse(res, baseOptions);
      })
    );
  }

  private processPublicResponse(data: any, options: ClassTransformOptions): any {
    // 处理数组返回场景(比如用户列表)
    if (Array.isArray(data)) {
      return data.map(item => this.singleObjectProcess(item, options));
    }
    // 处理单对象返回场景
    return this.singleObjectProcess(data, options);
  }

  private singleObjectProcess(obj: any, options: ClassTransformOptions): any {
    if (!obj || typeof obj !== 'object') return obj;
    // 先执行默认序列化(处理@Exclude()等原有逻辑)
    const serialized = instanceToPlain(obj, options);
    const proto = Object.getPrototypeOf(obj);
    // 遍历所有字段,删除公开路由下需要排除的字段
    Object.keys(serialized).forEach(prop => {
      const needExclude = Reflect.getMetadata(EXCLUDE_IF_PUBLIC_KEY, proto, prop);
      if (needExclude) delete serialized[prop];
    });
    // 递归处理嵌套对象
    Object.keys(serialized).forEach(key => {
      if (typeof serialized[key] === 'object' && serialized[key] !== null) {
        serialized[key] = this.processPublicResponse(serialized[key], options);
      }
    });
    return serialized;
  }
}
    1. 全局注册自定义拦截器
      替换Nest默认的序列化拦截器,在入口文件main.ts中配置:
import { NestFactory, Reflector } from '@nestjs/core';
import { AppModule } from './app.module';
import { ConditionalExcludeInterceptor } from './interceptors/conditional-exclude.interceptor';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const reflector = app.get(Reflector);
  // 全局启用自定义序列化拦截器
  app.useGlobalInterceptors(new ConditionalExcludeInterceptor(reflector));
  await app.listen(3000);
}
bootstrap();

配置完成后就可以直接在实体中使用你预期的装饰器写法:

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  @Exclude()
  password: string;

  @Column({ unique: true })
  @ExcludeIfPublic()
  email: string;

  @Column()
  username: string;
}

最终效果完全符合需求:

  • 加了@Public()装饰器的公开路由返回用户数据时,会自动排除password和email字段,避免敏感信息泄露
  • 走JWT认证的受保护路由返回用户数据时,只会排除password字段,email字段正常返回

如果你的接口返回结构有统一外层包装(比如{ code: 200, data: {}, msg: 'success' }),只需要在processPublicResponse方法中增加对包装层的判断,识别到业务数据字段后递归处理即可,逻辑和处理数组场景一致。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.03 04:42:40