You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 19:40:26