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

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 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消息,然后在写问卷出发写入事件,对应就会通知到刚订阅的位置了,这里就不演示了,我的演示需要两个用户,较复杂

最后


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



相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
28天前
|
设计模式 IDE API
C# 一分钟浅谈:GraphQL 客户端调用
本文介绍了如何在C#中调用GraphQL API,涵盖基本步骤、常见问题及解决方案。首先,通过安装`GraphQL.Client`库并创建客户端实例,连接到GraphQL服务器。接着,展示了如何编写查询和突变,以及处理查询语法错误、变量类型不匹配等常见问题。最后,通过具体案例(如管理用户和订单)演示了如何在实际项目中应用这些技术,帮助开发者更高效地利用GraphQL。
70 38
C# 一分钟浅谈:GraphQL 客户端调用
|
26天前
|
缓存 API C#
以C#一分钟浅谈:GraphQL 中的订阅与发布
本文从C#角度详细介绍了GraphQL中的订阅与发布机制,包括基本概念、实现方法、常见问题及解决方案。GraphQL订阅允许客户端实时接收服务器端的数据更新,适用于聊天应用、实时通知等场景。文中通过具体代码示例,展示了如何使用GraphQL.NET库实现订阅解析器和事件流,以及如何配置GraphQL服务和测试订阅功能。
27 5
|
29天前
|
消息中间件 JavaScript 前端开发
C# 一分钟浅谈:GraphQL 中的订阅与发布
本文介绍了 GraphQL 订阅与发布机制,重点从 C# 角度探讨其实现方法,包括基本概念、代码示例、常见问题及解决方案,旨在帮助开发者高效利用 GraphQL 实现实时数据更新。
26 3
|
2月前
|
消息中间件 网络协议 C#
C#使用Socket实现分布式事件总线,不依赖第三方MQ
`CodeWF.EventBus.Socket` 是一个轻量级的、基于Socket的分布式事件总线系统,旨在简化分布式架构中的事件通信。它允许进程之间通过发布/订阅模式进行通信,无需依赖外部消息队列服务。
C#使用Socket实现分布式事件总线,不依赖第三方MQ
|
6月前
|
消息中间件 存储 监控
中间件消息发布者功能特性
【6月更文挑战第11天】
50 5
|
6月前
|
设计模式 缓存 JavaScript
API设计模式:REST、GraphQL、gRPC与tRPC全面解析
API设计模式:REST、GraphQL、gRPC与tRPC全面解析
162 0
|
3月前
Nest.js 实战 (十三):实现 SSE 服务端主动向客户端推送消息
这篇文章介绍了在Nest.js应用中使用Server-Sent Events (SSE)的技术。文章首先讨论了在特定业务场景下,为何选择SSE而不是WebSocket作为实时通信系统的实现方式。接着解释了SSE的概念,并展示了如何在Nest.js中实现SSE。文章包含客户端实现的代码示例,并以一个效果演示结束,总结SSE在Nest.js中的应用。
100 1
Nest.js 实战 (十三):实现 SSE 服务端主动向客户端推送消息
|
4月前
|
中间件 API 网络架构
Django后端架构开发:从匿名用户API节流到REST自定义认证
Django后端架构开发:从匿名用户API节流到REST自定义认证
52 0
|
7月前
|
缓存 中间件 API
|
存储 JSON 缓存
JWT身份认证(附带源码讲解) | GO主题月
一天,正是午休时段 兵长路过胖sir座位,大吃一惊,今天胖sir居然没有打呼噜,而是在低着头聚精会神盯着一本书 兵长凑近一看,胖sir居然在看史书...
147 0