NestJS中如何动态为非公开路由添加Swagger授权响应装饰器?
我正在学习并开发一个NestJS个人项目,目前正在集成Swagger。若要标识某路由可能返回UnauthorizedException,通常需要手动添加@ApiUnauthorizedResponse装饰器,示例代码如下:
@ApiUnauthorizedResponse({ description: 'Unauthorized' }) @Get() findAll() { return this.usersService.findAll(); }
但我希望为所有非公开路由自动添加该装饰器。我设想通过Interceptor获取当前路由处理器及isPublic元数据,判断是否为非公开路由后为其添加装饰器,Interceptor的大致实现如下:
@Injectable() export class UnauthSwaggerInterceptor implements NestInterceptor { constructor(private readonly reflector: Reflector) {} intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const handler = context.getHandler(); const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC, [ context.getHandler(), context.getClass(), ]); if (!isPublic) { // Apply Swagger decorator to handler } return next.handle(); } }
其中,我通过Public装饰器标记公开路由:
export const IS_PUBLIC = 'isPublic'; export const Public = () => SetMetadata(IS_PUBLIC, true);
请问能否在运行时通过路由处理器引用动态添加该Swagger装饰器?若可行,正确的实现方式是什么?
直接在Interceptor里动态添加Swagger装饰器不可行,因为Swagger的装饰器是在应用启动时(编译阶段)解析元数据的,而Interceptor是运行时执行的,这时候Swagger已经完成了文档生成的扫描工作,动态添加的装饰器不会被识别。
正确的做法是在应用启动前,通过自定义装饰器或者扩展Swagger的扫描逻辑来批量添加@ApiUnauthorizedResponse,具体有两种实现方式:
方式一:自定义全局装饰器,批量处理非公开路由
创建一个自定义装饰器,结合Reflector判断路由是否为公开路由,自动为非公开路由添加Swagger响应装饰器:
import { applyDecorators, SetMetadata } from '@nestjs/common'; import { ApiUnauthorizedResponse } from '@nestjs/swagger'; import { Reflector } from '@nestjs/core'; export const IS_PUBLIC = 'isPublic'; export const Public = () => SetMetadata(IS_PUBLIC, true); // 自定义装饰器:自动为非公开路由添加Unauthorized响应 export const ApiProtectRoute = () => { return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { const reflector = new Reflector(); // 检查当前路由或控制器是否标记为Public const isPublic = reflector.getAllAndOverride<boolean>(IS_PUBLIC, [ descriptor.value, target, ]); if (!isPublic) { // 应用Swagger装饰器 ApiUnauthorizedResponse({ description: 'Unauthorized' })(target, propertyKey, descriptor); } }; };
在控制器上全局使用该装饰器,所有路由默认非公开,标记@Public()的路由则不会添加Unauthorized响应:
@Controller('users') @ApiProtectRoute() // 控制器级别全局应用 export class UsersController { @Get() findAll() { return this.usersService.findAll(); } @Public() // 标记为公开路由 @Get('public') getPublicData() { return 'public content'; } }
方式二:扩展Swagger文档生成逻辑,启动时批量处理
通过SwaggerModule的自定义配置,在生成文档前遍历所有路由元数据,为非公开路由添加响应定义:
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { INestApplication } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { IS_PUBLIC } from './public.decorator'; export function setupSwagger(app: INestApplication) { const config = new DocumentBuilder() .setTitle('API Docs') .setDescription('API documentation') .setVersion('1.0') .build(); const reflector = app.get(Reflector); const document = SwaggerModule.createDocument(app, config, { // 自定义操作处理器,批量修改路由元数据 operationFactory: (controllerKey: string, methodKey: string) => { const operation = SwaggerModule.createOperation(controllerKey, methodKey); const controller = app.get(controllerKey); const handler = controller[methodKey]; // 检查是否为公开路由 const isPublic = reflector.getAllAndOverride<boolean>(IS_PUBLIC, [ handler, controller.constructor, ]); if (!isPublic) { // 添加Unauthorized响应到Swagger操作定义 operation.responses['401'] = { description: 'Unauthorized', schema: { type: 'object', properties: { message: { type: 'string' } } }, }; } return operation; }, }); SwaggerModule.setup('api', app, document); }
在main.ts中调用该方法:
async function bootstrap() { const app = await NestFactory.create(AppModule); setupSwagger(app); await app.listen(3000); } bootstrap();
为什么Interceptor不可行?
Interceptor的intercept方法是在每次请求运行时执行的,而Swagger的元数据解析是在应用启动阶段完成的——也就是SwaggerModule.createDocument执行时就已经扫描完所有装饰器信息了。运行时动态添加的装饰器无法被Swagger的扫描逻辑捕获,所以不会出现在文档中。
内容的提问来源于stack exchange,提问作者Daniel Maranhao

