NestJS多租户集成Passport JWT异常:Request对象未定义
NestJS多租户:JwtStrategy验证阶段Request对象注入丢失问题
问题分析
核心问题是:请求作用域的数据库连接工厂在JwtStrategy的validate方法执行时,无法注入REQUEST对象(变为undefined),导致无法获取子域名建立租户连接,进而validateAccessToken无法执行完成。登录请求正常是因为登录流程未触发JWT守卫的验证逻辑,而登录后的请求会经过JWT守卫,此时连接工厂被二次触发时丢失了请求上下文。
可能的原因
- JWT守卫阶段的请求上下文传递问题:NestJS守卫执行时机早于控制器,通过
moduleRef.resolve获取请求作用域的AuthService时,请求上下文的关联可能存在遗漏,导致依赖的CONNECTION工厂无法正确注入REQUEST。 - 连接工厂未处理异步操作:如果
getTenantConnection是异步方法(比如创建TypeORM连接),但工厂的useFactory未使用async/await,会导致连接未正确初始化,进而引发后续错误。 - REQUEST注入的作用域冲突:JwtStrategy本身是单例作用域,在其内部解析请求作用域服务时,请求上下文的绑定可能未正确传递到依赖链的下游(即连接工厂)。
解决方案
方案1:使用AsyncLocalStorage存储租户上下文(推荐)
通过AsyncLocalStorage在请求早期存储子域名,避免后续依赖REQUEST注入,彻底解决上下文丢失问题:
- 创建租户上下文存储
// src/tenancy/tenancy.context.ts import { AsyncLocalStorage } from 'async_hooks'; export const tenancyStorage = new AsyncLocalStorage<string>();
- 添加租户中间件提取子域名
// src/tenancy/tenancy.middleware.ts import { Injectable, NestMiddleware } from '@nestjs/common'; import { Request, Response, NextFunction } from 'express'; import { tenancyStorage } from './tenancy.context'; @Injectable() export class TenancyMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { const subdomain = req.subdomains[0]; // 根据你的域名结构调整提取逻辑 if (subdomain) { tenancyStorage.run(subdomain, () => next()); } else { throw new Error('Missing tenant subdomain'); } } }
- 全局注册中间件
在AppModule的configure方法中注册:
configure(consumer: MiddlewareConsumer) { consumer.apply(TenancyMiddleware).forRoutes('*'); }
- 修改连接工厂使用上下文存储
export const connectionFactory = { provide: CONNECTION, scope: Scope.REQUEST, useFactory: async () => { const subdomain = tenancyStorage.getStore(); if (!subdomain) { throw new Error('Tenant subdomain not found in context'); } return await getTenantConnection(subdomain); // 确保异步操作使用await }, };
方案2:确保请求上下文正确传递到连接工厂
如果坚持使用REQUEST注入,需确保moduleRef.resolve时的上下文正确传递到依赖链:
- 标记AuthService为请求作用域
如果AuthService依赖请求作用域的CONNECTION,需将其也设为请求作用域:
@Injectable({ scope: Scope.REQUEST }) export class AuthService { constructor(@Inject(CONNECTION) private readonly connection: Connection) {} // ... }
- 修改连接工厂为异步
如果getTenantConnection是异步方法,必须让工厂返回Promise:
export const connectionFactory = { provide: CONNECTION, scope: Scope.REQUEST, useFactory: async (request: Request) => { console.log('request', request); if (!request) { throw new Error('Request object is undefined'); } const subdomain = request.subdomains[0]; // 调整子域名提取逻辑 if (!subdomain) { throw new Error('Missing tenant subdomain'); } return await getTenantConnection(subdomain); }, inject: [REQUEST], };
- 在JwtStrategy中显式传递上下文
确保moduleRef.resolve时正确使用请求上下文:
async validate(req: Request, payload: any): Promise<any> { const { jti } = payload; const contextId = ContextIdFactory.getByRequest(req); // 确保解析时传递上下文,并且AuthService是请求作用域 this.authService = await this.moduleRef.resolve(AuthService, contextId); try { const user = await this.authService.validateAccessToken(jti); console.log('here'); if (!user) { throw new UnauthorizedException(); } return user; } catch (error) { console.error('Error validating access token:', error); throw new UnauthorizedException(); } }
方案3:手动传递子域名到AuthService
绕过连接工厂的REQUEST依赖,直接在validate方法中提取子域名并传递给AuthService:
- 修改AuthService的validateAccessToken方法
async validateAccessToken(jti: string, subdomain: string) { const connection = await getTenantConnection(subdomain); // 使用connection查询用户 return connection.getRepository(User).findOne({ where: { tokenId: jti } }); }
- 在JwtStrategy中传递子域名
async validate(req: Request, payload: any): Promise<any> { const { jti } = payload; const subdomain = req.subdomains[0]; try { const user = await this.authService.validateAccessToken(jti, subdomain); console.log('here'); if (!user) { throw new UnauthorizedException(); } return user; } catch (error) { console.error('Error validating access token:', error); throw new UnauthorizedException(); } }
验证步骤
- 启动应用后,发送登录请求确认正常。
- 使用返回的token发送后续请求,检查是否输出
here日志。 - 查看数据库连接是否正确关联子域名对应的租户库。
内容的提问来源于stack exchange,提问作者Edison Biba
相关产品推荐
相关产品推荐

