Nest如何实现带身份验证的GraphQL订阅Subscription

简介: Nest如何实现带身份验证的GraphQL订阅Subscription前言最近在用nest+graphql做一个消息推送的功能,但发现相关资料真的好少,不仅官网文档没有详细介绍,社区中也少有人讨论,如下是笔者的一些配置,供大家参考,希望对你有所帮助。

Nest如何实现带身份验证的GraphQL订阅Subscription

前言


最近在用nest+graphql做一个消息推送的功能,但发现相关资料真的好少,不仅官网文档没有详细介绍,社区中也少有人讨论,如下是笔者的一些配置,供大家参考,希望对你有所帮助。

1. 安装graphql-redis-subscriptions


在Nest官方文档中,我们可以看到这样一句话:

NOTEPubSub is a class that exposes a simple publish and subscribe API. Read more about it here. Note that the Apollo docs warn that the default implementation is not suitable for production (read more here). Production apps should use a PubSub implementation backed by an external store (read more here).

简单来说就是阿波罗自带的pubsub并不推荐用户生产环境,主要原因就是不支持多台机器,目前比较主流的方案就是使用graphql-redis-subscriptions,所以,我们先从这里开始...

1)安装

npm i graphql-redis-subscriptions

2)docker-compose.yml增加redis配置,你也可以根据自己的需求自定义:

  redis:
    container_name: redis
    image: 'redis:alpine'
    ports:
      - 6379:${REDIS_PORT}


2. 新增PubSub文件



nest g mo pubsub 


然后在生成的文件中进行修改,如下是笔者修改该文件的内容:

import { Global, Module } from '@nestjs/common';
import { RedisPubSub } from 'graphql-redis-subscriptions';
import { ConfigService, ConfigModule } from '@nestjs/config';
export const PUB_SUB = 'PUB_SUB';
@Global()
@Module({
  imports: [ConfigModule],
  providers: [
    {
      provide: PUB_SUB,
      inject: [ConfigService],
      useFactory: (configService: ConfigService) =>
        new RedisPubSub({
          connection: {
            host: configService.get('REDIS_HOST'),
            port: configService.get('REDIS_PORT'),
          },
        }),
    },
  ],
  exports: [PUB_SUB],
})
export class PubsubModule {}


3. App.module中配置


@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true, load: [config] }),
    PrismaModule.forRoot({
      isGlobal: true,
      prismaServiceOptions: {
        middlewares: [loggingMiddleware(new Logger('PrismaMiddleware'))], // configure your prisma middleware
      },
    }),
    AuthModule,
    GraphQLModule.forRootAsync<ApolloDriverConfig>({
      imports: [AuthModule],
      driver: ApolloDriver,
      useClass: GqlConfigService, // 下面有
      inject: [AuthService],
    }),
    ]
    // 略...
})

其中AuthModule是做身份验证的模块,因为笔者使用的是JWT方案,后续会使用该AuthService进行用户信息的解析。

4. 新增GqlConfigService配置


我是在app.module同级目录新增了gql-config.service.ts这个文件,注意其中的subscriptions: {...}和context配置就可以了,其他的都是一些常见的配置,自己根据官网自定义,如下是文件内容:


import { GraphqlConfig } from './common/configs/config.interface';
import { ConfigService } from '@nestjs/config';
import { ApolloDriverConfig } from '@nestjs/apollo';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { GqlOptionsFactory } from '@nestjs/graphql';
import { ApolloServerPluginLandingPageLocalDefault } from 'apollo-server-core';
import { AuthService } from 'src/auth/auth.service';
@Injectable()
export class GqlConfigService implements GqlOptionsFactory {
  constructor(
    private configService: ConfigService,
    private authService: AuthService
  ) {}
  createGqlOptions(): ApolloDriverConfig {
    const graphqlConfig = this.configService.get<GraphqlConfig>('graphql');
    return {
      // schema options
      autoSchemaFile: graphqlConfig.schemaDestination || './src/schema.graphql',
      sortSchema: graphqlConfig.sortSchema,
      buildSchemaOptions: {
        numberScalarMode: 'integer',
      },
      // subscription
      subscriptions: {
        'graphql-ws': {
          // websocket身份校验,并附带user
          onConnect: async (context: any) => {
            const { connectionParams, extra } = context;
            // user validation will remain the same as in the example above
            // when using with graphql-ws, additional context value should be stored in the extra field
            const authorization = connectionParams?.Authorization;
            const token = authorization?.split(' ')?.pop();
            try {
              const user = await this.authService.getUserFromToken(token);
              extra.user = user;
              extra.headers = { authorization };
            } catch (err) {
              throw new UnauthorizedException();
            }
            return context;
          },
        },
      },
      debug: graphqlConfig.debug,
      playground: graphqlConfig.playgroundEnabled,
      plugins: [ApolloServerPluginLandingPageLocalDefault()],
      context: ({ req, extra }) => ({
        req,
        extra,
      }),
    };
  }
}


在这里,我将token提取并解析成一个完整的用户对象,并挂载到了extra对象上方便后续使用。

5. 修改身份校验装饰器


笔者这里使用的JWT身份校验是@nestjs/passport,然后自定义了一个装饰器用户接口的身份校验,带上该装饰器就必须是登录的用户,否则则是所有用户都可以请求该接口:

// src/auth/gql-auth.guard.ts
import { Injectable, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GqlExecutionContext } from '@nestjs/graphql';
@Injectable()
export class GqlAuthGuard extends AuthGuard('jwt') {
  getRequest(context: ExecutionContext) {
    const ctx = GqlExecutionContext.create(context).getContext();
    return ctx.req || ctx.extra; // ws的也装载headers供JWT校验
  }
}

6.修改用户实体装饰器


这个装饰器的作用是获取用户实体信息,装饰在参数上,这两个装饰器后续会简单介绍如何使用:

// src/common/user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
export const UserEntity = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const context = GqlExecutionContext.create(ctx).getContext();
    return context?.req?.user || context?.extra?.user;
  }
);

可以使用了(演示)


这里,我在其中一个resolve.ts文件中进行简单演示:

// 略..
import { RedisPubSub } from 'graphql-redis-subscriptions';
import { PUB_SUB } from 'src/pubsub/pubsub.module';
enum SUBSCRIPTION_EVENTS {
  haveWritten = 'haveWritten',
}
@Resolver(() => User2questionnaire)
export class User2questionnaireResolver {
  constructor(
    private prisma: PrismaService,
    private readonly user2questionnaireService: User2questionnaireService,
    @Inject(PUB_SUB) private readonly pubSub: RedisPubSub
  ) {}
  @UseGuards(GqlAuthGuard) // 携带UseGuards这个装饰器就必须包含登录态才能请求这个接口
  @Subscription(() => User2questionnaire)
  haveWritten(@UserEntity() user: User) {  // 不用传入user,就可以直接使用user相关参数
    return this.pubSub.asyncIterator(
      `${SUBSCRIPTION_EVENTS.haveWritten}.${user.id}` // 每个用户各包含动态事件
    );
  }
// 略...
  @UseGuards(GqlAuthGuard)
  @Mutation(() => User2questionnaire)
  async write(@UserEntity() user: User, @Args('data') data: CreateU2QInput) {
    const newU2Q = await this.user2questionnaireService.create(user, data);
    // pub
    const { ownerId, friendId } = data;
    // 自己填写就不进行通知
    if (ownerId !== friendId) {
      this.pubSub.publish(`${SUBSCRIPTION_EVENTS.haveWritten}.${ownerId}`, {
        haveWritten: newU2Q,
      });
    }
    return newU2Q;
  }


然后我们在对应的阿波罗playground就可以先订阅,会出现listening消息,然后在写问卷出发写入事件,对应就会通知到刚订阅的位置了,这里就不演示了,我的演示需要两个用户,较复杂

最后


上述配置仅供参考,基本上都是笔者自己逐步摸索出来的,并没有在任何官方文档中找到对应的肯定的配置描述,所以不一定是最佳实践,请根据自己的需求自行修改。



目录
相关文章
|
存储 人工智能 自然语言处理
|
Cloud Native Linux 虚拟化
【云原生】Docker容器资源限制(CPU/内存/磁盘)
【云原生】Docker容器资源限制(CPU/内存/磁盘)
3403 0
|
11月前
|
SQL 安全 关系型数据库
PostgreSQL 18 发布
PostgreSQL 18重磅升级:新增异步I/O提升高并发性能,原生支持UUID v7增强索引效率,并优化并行查询、逻辑复制,强化安全与SQL功能,全面提升数据库性能与可靠性。
502 8
|
开发框架 前端开发 JavaScript
【HarmonyOS Next之旅】基于ArkTS开发(二) -> UI开发一
本文介绍了方舟开发框架(ArkUI)及其两种开发范式:基于ArkTS的声明式开发范式和类Web开发范式。ArkUI是用于构建HarmonyOS应用界面的UI框架,提供极简UI语法和基础设施。声明式开发范式使用ArkTS语言,以组件、动画和状态管理为核心,适合复杂团队协作;类Web开发范式采用HML、CSS、JavaScript三段式开发,适用于简单界面应用,贴近Web开发者习惯。文中还概述了两者的架构和基础能力,帮助开发者选择合适的范式进行高效开发。
546 15
|
10月前
|
监控 数据可视化 数据挖掘
1688运营实战指南:1688系统化学习与进阶技巧
本文系统解析1688平台运营核心策略,涵盖店铺建设、流量获取、转化提升、数据分析与客户管理五大模块,助力企业打造专业形象,实现精准引流、高效转化与持续复购,推动B2B电商业务稳步增长。
|
11月前
|
人工智能 自然语言处理 JavaScript
Playwright MCP在UI回归测试中的实战:构建AI自主测试智能体
Playwright MCP结合AI智能体,革新UI回归测试:通过自然语言驱动浏览器操作,降低脚本编写门槛,提升测试效率与覆盖范围。借助快照解析、智能定位与Jira等工具集成,实现从需求描述到自动化执行的闭环,推动测试迈向智能化、民主化新阶段。
在数据驱动方式中处理复杂的数据结构
【10月更文挑战第13天】 在数据驱动的开发模式中,处理复杂数据结构是一项重要任务。本文从理解特性、数据分解、选择模型、数据绑定、转换预处理、处理嵌套、性能优化、错误处理、数据验证及实际案例等方面,详细阐述了应对这一挑战的方法和策略,强调了持续学习和改进的重要性。
675 157
|
人工智能 前端开发 关系型数据库
过年了,用魔搭+魔笔打造您的 AI 春节贺卡生成器!
本文介绍了如何获取和利用现有的大模型资源,结合魔笔低代码,低成本、高效率地打造一个 AI 春节贺卡生成器。
|
Android开发
无法唤起支付宝APP问题分析
商家在网页中调用支付宝提供的网页支付接口调起支付宝客户端内的支付模块,商家网页会跳转到支付宝中完成支付,支付完后跳回到商家网页内,最后展示支付结果。若无法唤起支付宝客户端,则在一定的时间后会自动进入网页支付流程。
10063 12
|
监控 网络协议 安全
在Linux中,如何进行系统性能的峰值测试?
在Linux中,如何进行系统性能的峰值测试?