NestJS中跨GRPC/GraphQL向服务透传租户ID的正确方案
NestJS 跨gRPC/GraphQL场景统一透传租户ID最佳实践
这套方案可以实现零手动传参、零重复解析代码、全协议兼容,不需要在服务层直接操作ExecutionContext,也不需要写冗余的透传逻辑,完全符合NestJS设计规范。
优先方案:基于AsyncLocalStorage实现全局请求上下文(无性能损耗)
Node.js原生的AsyncLocalStorage可以在异步调用链中独立存储请求级数据,完全和上层协议解耦,不需要依赖Nest的请求作用域注入(请求作用域会导致服务实例每次请求重建,高并发下有明显性能开销)。
实现步骤
- 第一步:编写单例上下文服务
import { Injectable } from '@nestjs/common'; import { AsyncLocalStorage } from 'async_hooks'; export interface TenantInfo { token: string; id: string; } @Injectable() export class RequestContextService { private readonly als = new AsyncLocalStorage<TenantInfo>(); // 拦截器/守卫层调用,初始化当前请求的租户信息 runWithTenant(tenantInfo: TenantInfo, callback: () => void) { this.als.run(tenantInfo, callback); } // 服务层直接调用获取租户信息,不需要传任何参数 getTenant(): TenantInfo { const tenant = this.als.getStore(); if (!tenant) { throw new Error('Cannot get tenant info outside request scope'); } return tenant; } }
- 第二步:编写全局守卫,在请求入口统一解析租户ID
你已经实现的getTenantId多协议解析逻辑直接放在全局守卫里即可,所有请求进来先解析租户信息,存入ALS上下文,后续业务逻辑完全不需要再处理解析逻辑:
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; import { GqlContextType, GqlExecutionContext } from '@nestjs/graphql'; import { RequestContextService } from './request-context.service'; // 引入已有的parseTokenTenantInfo、logger方法即可 @Injectable() export class TenantAuthGuard implements CanActivate { constructor(private readonly ctxService: RequestContextService) {} canActivate(context: ExecutionContext): Promise<boolean> { return new Promise((resolve) => { const tenantInfo = this.parseTenantFromContext(context); this.ctxService.runWithTenant(tenantInfo, () => { resolve(true); }); }); } private parseTenantFromContext(context: ExecutionContext) { if (context.getType() === 'rpc') { logger.debug('received rpc request'); const rpcCtx = context.switchToRpc().getContext(); const token = rpcCtx.get("x-authorization"); return { token, id: parseTokenTenantInfo(token) }; } if (context.getType<GqlContextType>() === 'graphql') { logger.debug('received graphql request'); const gqlCtx = GqlExecutionContext.create(context); const request = gqlCtx.getContext().request; const token = request.headers.authorization; return { token, id: parseTokenTenantInfo(token) }; } throw new Error(`Unknown context type receiving in tenant guard`); } }
- 第三步:全局注册守卫,确保所有请求都经过租户解析
在公共模块中注册全局守卫即可,不需要在每个Controller/Resolver上单独添加:
import { APP_GUARD } from '@nestjs/core'; @Module({ providers: [ RequestContextService, { provide: APP_GUARD, useClass: TenantAuthGuard, }, ], exports: [RequestContextService], }) export class CommonModule {}
- 业务层使用方式
所有服务保持单例,直接注入RequestContextService就能拿到当前请求的租户信息,完全不需要手动传参,也不需要关心当前请求是gRPC还是GraphQL:
@Injectable() export class MyService { constructor(private readonly ctxService: RequestContextService) {} myMethod1() { // 直接获取租户信息,无额外传参 const { id: tenantId, token } = this.ctxService.getTenant(); // 后续业务逻辑直接使用tenantId即可 } }
备选方案:适配REQUEST注入的请求作用域上下文
如果项目不方便使用AsyncLocalStorage,可以通过适配GraphQL上下文的方式解决@Inject(REQUEST)不兼容GraphQL的问题,缺点是依赖这个上下文的服务都会变成请求作用域,高并发场景下有性能损耗。
核心配置
首先在GraphQL模块配置中,把request对象挂到上下文根节点,确保@Inject(REQUEST)能拿到统一结构的请求:
GraphQLModule.forRoot({ // 其他配置省略 context: ({ req }) => ({ req }), })
然后编写统一的租户上下文提供者,在工厂函数中统一解析租户信息,服务层直接注入解析好的租户信息即可,不需要自己处理ExecutionContext:
@Module({ providers: [ { provide: 'TENANT_INFO', scope: Scope.REQUEST, inject: [REQUEST, CONTEXT], useFactory: (req, context: ExecutionContext) => { // 复用已有的getTenantId解析逻辑即可 return getTenantId(context); }, }, ], exports: ['TENANT_INFO'], }) export class CommonModule {}
服务层使用时直接通过@Inject('TENANT_INFO')注入即可拿到租户信息,不需要重复编写解析逻辑。
方案优势
- 所有租户解析逻辑统一收敛在请求入口层,完全消除重复的解析、透传代码
- 机制上保证只要请求经过全局守卫,租户信息一定存在,从根源避免漏传问题
- 业务层完全和协议解耦,不需要关心当前请求来源是gRPC还是GraphQL
- 优先推荐的ALS方案没有请求作用域的性能问题,所有服务保持单例,高并发场景表现稳定
内容的提问来源于stack exchange,提问作者Fred Johnson
相关产品推荐
相关产品推荐

