如何为带路由的Express中间件创建NestJS中间件包装器?
Auth0提供的express-openid-connect是一款自带路由的全局Express中间件,直接在main.ts里通过app.use(auth({...}))注册时,/login、/callback这类自带路由能正常工作。但用NestMiddleware包装后,这些路由返回404,即使开启DEBUG模式确认中间件包含这些路由也无法解决问题。
已实现的中间件包装代码:
import { Inject, Injectable, NestMiddleware } from '@nestjs/common'; import { RequestHandler } from 'express'; import { auth } from 'express-openid-connect'; import { OidcOptions, OIDC_MODULE_OPTIONS } from './oidc'; @Injectable() export class OidcMiddleware implements NestMiddleware { private middleware: RequestHandler; constructor(@Inject(OIDC_MODULE_OPTIONS) oidcOptions: OidcOptions) { this.middleware = auth({ ...oidcOptions.configParams }) } use(req: any, res: any, next: () => void) { this.middleware(req, res, next); } }
模块应用代码:
@Module({ imports: [HttpModule], controllers: [OidcController] }) export class OidcModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer .apply(OidcMiddleware) .forRoutes('*') } }
尝试过在虚拟控制器上注册路由,但请求对象req被重写,手动调用逻辑繁琐且不规范,想了解NestJS开发者通常如何处理这类带路由的Express中间件包装。
解决方案
1. 直接在NestJS实例上挂载中间件(推荐)
NestJS的INestApplication实例本质封装了Express的app对象,直接在main.ts里挂载这类带路由的中间件是最简单可靠的方式,完全复用纯Express环境下的用法:
// main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { auth } from 'express-openid-connect'; import { OidcOptions } from './oidc/oidc'; async function bootstrap() { const app = await NestFactory.create(AppModule); // 配置Auth0参数 const oidcConfig: OidcOptions['configParams'] = { issuerBaseURL: '你的Auth0 issuer地址', baseURL: '你的应用基础地址', clientID: '你的客户端ID', secret: '你的客户端密钥', // 其他配置项 }; // 直接挂载中间件 app.use(auth(oidcConfig)); await app.listen(3000); } bootstrap();
这种方式下,中间件自带的路由会被Express直接注册,不会受NestJS路由匹配优先级的影响,/login、/callback等路由可以正常生效。
2. 封装成NestJS动态模块(符合模块化规范)
如果希望将OIDC配置和中间件封装为可复用模块,避免在main.ts中写入业务配置,可以创建动态模块,在模块的onModuleInit钩子中获取Nest应用实例并挂载中间件:
// oidc.module.ts import { DynamicModule, Module, OnModuleInit } from '@nestjs/common'; import { NestExpressApplication } from '@nestjs/platform-express'; import { auth } from 'express-openid-connect'; import { OidcOptions } from './oidc'; @Module({}) export class OidcModule implements OnModuleInit { constructor(private readonly app: NestExpressApplication) {} static forRoot(options: OidcOptions): DynamicModule { return { module: OidcModule, providers: [ { provide: 'OIDC_OPTIONS', useValue: options, }, ], }; } async onModuleInit() { const oidcOptions = this.app.get('OIDC_OPTIONS') as OidcOptions; this.app.use(auth(oidcOptions.configParams)); } }
然后在根模块中导入:
// app.module.ts import { Module } from '@nestjs/common'; import { OidcModule } from './oidc/oidc.module'; @Module({ imports: [ OidcModule.forRoot({ configParams: { // 你的Auth0配置参数 }, }), ], }) export class AppModule {}
失效原因说明
NestJS的MiddlewareConsumer是给Nest自身注册的路由绑定中间件的,中间件只会在Nest路由匹配成功后执行。而express-openid-connect自带的路由是直接注册到Express路由栈中的,这些路由并未在Nest的路由体系中注册,所以请求打到这些路由时,Nest会先做路由匹配,找不到对应控制器就直接返回404,不会触发你挂载的中间件。
内容的提问来源于stack exchange,提问作者Robin De Schepper

