← 中文资源

简体中文版本

基于 WebSocket 构建 GraphQL 订阅服务

一个完整的 GraphQL 订阅服务:WebSocket 传输、连接时鉴权、发布订阅集成、背压与投递测试。

阅读英文原文

项目概述

本项目基于 WebSocket 实现 GraphQL 订阅:在连接时完成鉴权,跟踪每个订阅者的用户与主题,并在每个事件上重新校验授权,使订阅者只会收到其有权看到的事件。长连接的安全模型必须在其整个生命周期内成立,而不只在握手时。

架构与关键决策

连接在初始化阶段鉴权并把身份附加到连接上,订阅注册表把主题映射到连接,从而移除订阅或吊销用户会在下一个事件生效而非下次重连。授权按事件执行,因为连接可能比创建它的权限更长寿。扇出通过共享发布订阅通道跨实例进行,内存路径仅用于单实例并明确记录;每订阅者队列有界,跟不上者被关闭而非拖住广播者。

实施与测试

以两个真实 WebSocket 连接断言:已鉴权订阅者能收到其主题事件,而第二个用户不会收到他人事件;订阅后吊销权限断言下一事件被过滤;填满队列断言连接被关闭而非广播者阻塞。

限制与后续

  • 每连接至多一次投递,无断线后的重放。
  • 授权按当前规则逐事件重算,但无变更推送。
  • 背压丢弃慢客户端而非无限缓冲。

代码示例

pnpm init
pnpm add graphql-yoga graphql-ws ioredis
pnpm add -D typescript tsx vitest
graphql-subscriptions/
├── src/server.ts
├── src/auth.ts
├── src/pubsub.ts
├── src/resolvers.ts
├── tests/subscription.test.ts
└── package.json
const user = await verify(connectionParams.token);
if (!user) return false; // close the socket
const iterator = pubsub.subscribe('order:' + orderId);
for await (const e of iterator) { if (allowed(user, e)) yield e; }
await publisher.publish('order:' + id, JSON.stringify(event));
if (socket.bufferedAmount > MAX) return coalesce(event);
pnpm vitest run