NestJS认证中如何全局向CRUD端点传递UserId避免重复传参
解决重复提取userId透传逻辑的方案
两种方案都不需要改动你现有的JWT认证、Prisma查询核心逻辑,按需选择即可:
方案1:自定义参数装饰器(首推,零性能损耗)
把从请求对象拆取userId的逻辑封装成Nest原生参数装饰器,控制器里不用再写注入Req、取属性的重复代码。
先创建装饰器文件:
// src/common/decorators/current-user-id.decorator.ts import { createParamDecorator, ExecutionContext } from '@nestjs/common'; export const CurrentUserId = createParamDecorator( (_: unknown, ctx: ExecutionContext) => { const req = ctx.switchToHttp().getRequest(); return req.user?.userId; }, );
控制器里直接注入使用,原有服务层代码完全不需要修改:
export class BoardsController { // ... @Get() findAll(@CurrentUserId() userId: string) { return this.boardsService.findAll(userId); } // ... }
这个方案改动量最小,没有额外性能开销,适合绝大多数业务场景。
方案2:请求上下文自动透传(适合深层服务调用场景)
如果项目分层多,不想在控制器、服务层之间反复传递userId,可以用AsyncLocalStorage做全局请求上下文,自动透传用户信息。
首先创建请求上下文服务:
// src/common/request-context/request-context.service.ts import { Injectable, Scope } from '@nestjs/common'; import { AsyncLocalStorage } from 'async_hooks'; @Injectable({ scope: Scope.REQUEST }) export class RequestContextService { private readonly als = new AsyncLocalStorage<{ userId: string }>(); setUserId(userId: string) { this.als.enterWith({ userId }); } getUserId(): string | undefined { return this.als.getStore()?.userId; } }
再写全局拦截器,请求进入时自动把认证后的userId存入上下文:
// src/common/request-context/request-context.interceptor.ts import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable } from 'rxjs'; import { RequestContextService } from './request-context.service'; @Injectable() export class RequestContextInterceptor implements NestInterceptor { constructor(private readonly ctxService: RequestContextService) {} intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const req = context.switchToHttp().getRequest(); if (req.user?.userId) { this.ctxService.setUserId(req.user.userId); } return next.handle(); } }
在根模块全局注册拦截器:
@Module({ imports: [/* 原有导入配置 */], controllers: [AppController], providers: [ // 原有JWT全局守卫 { provide: 'APP_GUARD', useClass: JwtAuthGuard, }, // 新增上下文拦截器 { provide: APP_INTERCEPTOR, useClass: RequestContextInterceptor, }, ], })
之后控制器不用传userId,服务层直接从上下文服务取值即可:
// 控制器简化后 @Get() findAll() { return this.boardsService.findAll(); } // 服务层直接获取当前用户ID @Injectable() export class BoardsService { constructor(private readonly ctxService: RequestContextService) {} findAll() { const userId = this.ctxService.getUserId(); return prisma.board.findMany({ where: { userId }, }); } }
注意:请求作用域的服务会让依赖它的服务也变为请求作用域,超高并发场景如果对性能延迟敏感优先选方案1,普通业务系统用这个方案完全没有问题。
选型参考
- 只需要消除控制器里重复取userId的样板代码,直接选方案1,简单靠谱
- 项目分层深、多服务调用时不想层层透传userId,选方案2,代码更简洁
内容的提问来源于stack exchange,提问作者Guy
相关产品推荐
相关产品推荐

