NestJS GraphQL登录后如何通过Interceptor设置Context并生效?
问题
我希望让@Context存储tokens、用户等认证信息。已创建TGqlContext类型并在初始化GraphQL时传入:
export type TGqlContext = { req: Request; res: Response; accessToken: string; refreshToken: string; user?: UserDTO | undefined; }; GraphQLModule.forRoot<ApolloDriverConfig>({ driver: ApolloDriver, include: [ProductsModule, AuthModule, TransactionsModule], buildSchemaOptions: { dateScalarMode: "timestamp", }, autoSchemaFile: true, context: (ctx: TGqlContext) => ctx, }),
登录成功后尝试用Interceptor将认证数据存入Context,虽然登录Resolver的结果能被正常拦截且数据不为undefined,但在后续查询中使用@Context时,Context数据仍为undefined。
登录Mutation及Interceptor代码如下:
登录Mutation
@UseInterceptors(LoggingInterceptor) @Mutation(() => AuthDTO) async login(@Args("credentials") credentials: AuthLoginInputType): Promise<AuthDTO> { const log = await this.authService.login(credentials); if (log.isValid) { this.authService.accessToken = log.accessToken; this.authService.refreshToken = log.refreshToken; this.authService.user = log.user; return { accessToken: log.accessToken, refreshToken: log.refreshToken, user: log.user, }; } else { this.authService.accessToken = ""; this.authService.refreshToken = ""; this.authService.user = undefined; throw new HttpException(`${log.error} - ${log.message}`, log.httpStatus); } }
Interceptor代码
export class LoggingInterceptor implements NestInterceptor { private readonly logger = new Logger(LoggingInterceptor.name); constructor() { this.logger.log("inter"); } intercept(context: ExecutionContext, next: CallHandler): Observable<any> | Promise<Observable<any>> { const ctx = GqlExecutionContext.create(context).getContext<TGqlContext>(); const resu = next.handle().pipe( map((res: TGqlContext) => { this.logger.log(res.accessToken); //<--- 此处数据不为undefined ctx.accessToken = res.accessToken; ctx.refreshToken = res.accessToken; ctx.user = res.user; return res; }), catchError((err: any) => { throw new HttpException(err.message, HttpStatus.BAD_REQUEST); }) ); return resu; } }
尝试获取Context数据的查询示例:
@Query(() => AuthDTO) async me(@Context() ctx: TGqlContext): Promise<AuthDTO> { this.logger.log(ctx.accessToken); // <--- 此处数据为undefined... const mee: AuthDTO = { accessToken: this.authService.accessToken, refreshToken: this.authService.refreshToken, user: this.authService.user, }; return mee; }
解决方案
1. 核心问题:Context的生命周期
Nest GraphQL的Context是请求级别的——每个请求都会生成全新的Context实例。你在登录请求的Interceptor里设置的Context值,只对当前登录请求有效,后续的me查询是新请求,会创建新的Context,之前的自然不存在。
另外,你在authService中存储Token和用户的方式完全错误:Nest服务默认是单例模式,多个用户请求共享同一个实例,会导致用户信息互相覆盖,引发严重安全问题,必须删除这部分代码。
2. 正确的实现方式
方式一:用HTTP Cookie存储Token(推荐)
登录成功后把Token写入Cookie,后续请求会自动携带Cookie,然后在Context工厂函数中提取并验证Token,注入用户信息:
// 修改GraphQLModule的context配置 GraphQLModule.forRoot<ApolloDriverConfig>({ driver: ApolloDriver, include: [ProductsModule, AuthModule, TransactionsModule], buildSchemaOptions: { dateScalarMode: "timestamp", }, autoSchemaFile: true, context: async ({ req, res }) => { // 从Cookie提取Token const accessToken = req.cookies['accessToken']; const refreshToken = req.cookies['refreshToken']; let user: UserDTO | undefined; // 验证Token并获取用户信息(根据你的authService逻辑实现) if (accessToken) { user = await this.authService.validateToken(accessToken); } return { req, res, accessToken, refreshToken, user }; }, }),
登录Mutation中写入Cookie:
@Mutation(() => AuthDTO) async login( @Args("credentials") credentials: AuthLoginInputType, @Context() ctx: TGqlContext ): Promise<AuthDTO> { const log = await this.authService.login(credentials); if (log.isValid) { // 写入Cookie,配置安全属性 ctx.res.cookie('accessToken', log.accessToken, { httpOnly: true, // 防止XSS攻击 secure: process.env.NODE_ENV === 'production', // 生产环境强制HTTPS maxAge: 15 * 60 * 1000, // 15分钟有效期 }); ctx.res.cookie('refreshToken', log.refreshToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', maxAge: 7 * 24 * 60 * 1000, // 7天有效期 }); return { accessToken: log.accessToken, refreshToken: log.refreshToken, user: log.user, }; } else { throw new HttpException(`${log.error} - ${log.message}`, log.httpStatus); } }
后续me查询就能直接从Context获取数据:
@Query(() => AuthDTO) async me(@Context() ctx: TGqlContext): Promise<AuthDTO> { return { accessToken: ctx.accessToken, refreshToken: ctx.refreshToken, user: ctx.user, }; }
方式二:用请求头传递Token
如果不使用Cookie,让前端登录后把accessToken放在请求头(比如Authorization: Bearer <token>)中,然后在Context工厂函数解析:
context: async ({ req, res }) => { const authHeader = req.headers.authorization; let accessToken = ''; let user: UserDTO | undefined; if (authHeader && authHeader.startsWith('Bearer ')) { accessToken = authHeader.slice(7); user = await this.authService.validateToken(accessToken); } return { req, res, accessToken, user }; },
3. 移除无用的Interceptor
你用Interceptor在登录请求中设置Context的做法没有意义,因为Context无法跨请求复用。Interceptor适合做日志、请求参数修改等场景,不要用来存储认证信息。
内容的提问来源于stack exchange,提问作者Jerome

