NestJs如何实现公开路由下条件排除实体属性的装饰器
实现方案
基于class-transformer序列化逻辑+Nest元数据能力即可实现,不需要改动现有JWT认证、@Public()装饰器的原有逻辑,分三步落地:
- 实现
@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); }; };
- 自定义序列化拦截器,替换默认的
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; } }
- 全局注册自定义拦截器
替换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
相关产品推荐
相关产品推荐

