NestJS WebSocket 实战:鉴权、房间授权与可靠消息
本文目录
实时通信不是把 REST API 换成 socket.emit()。连接会长期存在,权限可能在连接期间变化,网络会中断,事件会重复或丢失,多实例也会让进程内状态失效。本文以 NestJS Gateway + Socket.IO 为例,说明从协议选择到生产运维的完整边界。示例是结构模板,接入前要按项目的认证、DTO、数据库和版本重新验证。
1. 先选对传输方式
- 原生 WebSocket 提供双向帧传输,协议简单、额外语义少;重连、事件命名、ACK、房间和兼容处理由应用自行实现。
- Socket.IO 是建立在 Engine.IO 之上的事件通信库,不等同于原生 WebSocket。它包含重连、ACK、房间和传输升级等能力,也可能先用 HTTP long-polling 再升级。浏览器的原生
WebSocket客户端不能直接连接 Socket.IO 服务端。 - Server-Sent Events 适合服务端单向推送、客户端通过 HTTP 发命令的场景,例如日志或生成进度;它通常更简单,也更容易沿用 HTTP 鉴权和代理。
如果需求只是“服务端推送状态”,先比较 SSE;只有双向低延迟、长连接或多路实时事件确实有价值时,再引入 WebSocket/Socket.IO。不要为了技术新鲜感把每个通知都改成长连接。
2. 把握手、事件、领域服务分开
Gateway 负责连接生命周期和事件协议;DTO/Pipe 校验消息形状;Guard 或连接中间件识别身份;领域 Service 负责资源授权与业务事务;数据库/outbox 保存需要可靠交付的事实。Gateway 不应直接拼 SQL、维护唯一业务状态或把任意客户端参数广播给其他人。
Nest Gateway 是 provider,可以注入服务,并有 init、connect、disconnect 生命周期。示例只展示依赖方向和结构:
@WebSocketGateway({ namespace: '/orders', cors: { origin: ['https://blog.example.com'] } })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true }))
export class OrderGateway implements OnGatewayConnection, OnGatewayDisconnect {
constructor(private readonly orders: OrdersService) {}
async handleConnection(client: Socket) {
// 握手认证在连接 middleware/guard 中完成;这里只处理已认证连接。
}
handleDisconnect(client: Socket) {
// 释放进程外资源时要幂等;不能把 disconnect 当作业务交易已回滚。
}
@SubscribeMessage('order.watch')
async watchOrder(
@ConnectedSocket() client: AuthenticatedSocket,
@MessageBody() input: WatchOrderDto,
) {
await this.orders.assertCanRead(client.data.userId, input.orderId);
await client.join(`user:${client.data.userId}:order:${input.orderId}`);
return { event: 'order.watch.ack', data: { orderId: input.orderId } };
}
}
AuthenticatedSocket、DTO 和认证插件应由应用实现,不能照抄为存在的 Nest 内建类型。还要确认 Nest WebSocket adapter 的异常映射:消息处理应返回明确 ACK/错误事件;不要把内部异常、数据库信息或堆栈发给客户端。
3. 身份只在握手验证还不够
不要把长期 token 放在 URL query:它容易进入代理访问日志、分析系统和错误报告。Socket.IO 支持客户端通过 auth 负载传递凭据;服务端在连接中间件验证签名、过期时间、签发方/受众,并从数据库确认账号有效和当前角色。拒绝连接时返回通用错误,不能泄露“账号存在但被封禁”等信息。
连接 middleware 通常每条连接运行一次,不会自动在每条消息上重验。长连接期间可能发生登出、封禁、角色降级或 token 撤销。需要按风险选择:设置最大连接寿命、定时检查撤销版本、接收服务端强制下线信号;每个敏感事件仍重新检查当前资源权限。角色变化后,旧连接不能继续依赖握手时缓存的角色。
token 既不要写入日志,也不要在异常信息、指标标签或 socket.data 中不必要地保留完整凭据。认证成功后仅保存后续业务需要的稳定用户 ID、认证版本等最小上下文。
4. 房间名不是授权凭证
Socket.IO room 是服务端用于分组广播的机制;客户端不应能通过提供 roomId 就加入任意房间。正确顺序是:解析并验证业务 ID、加载资源、按当前用户做租户/成员/所有者检查,成功后由服务端生成房间名并执行 join。退出、账号封禁、成员移除和权限变更时,要考虑主动离开相关房间或断开连接。
命名空间用于协议隔离,不等于权限边界;隐藏的频道名也不是 secret。即使用户已经加入某个 room,每个会改变数据的事件仍要校验权限。避免 client.join(input.room)、客户端指定接收者 ID 后直接私发,以及把房间成员表暴露给普通用户。
5. 验证、限流和输出边界
- 对事件名使用显式白名单;DTO 限制类型、长度、数组元素数和嵌套深度,拒绝多余字段。对二进制帧限制大小并单独约束上传流量。
- 连接限流和事件限流分开:按 IP 限新连接,按用户/事件限业务操作;代理后的 IP 只信任已配置的反向代理。需要限制并发连接数、消息速率和服务端发送队列,避免慢客户端积压耗尽内存。
- ACK 只确认处理阶段,不默认代表数据库事务、邮件或第三方副作用已完成。为昂贵操作设超时/取消边界,把任务转入持久队列。
- CORS 主要约束浏览器的 HTTP long-polling;浏览器 WebSocket 握手不受 CORS 机制同样约束。检查 Origin、配置 allowRequest/网关 ACL,并在服务端鉴权。CORS 不是身份认证,更不是抵御非浏览器客户端的防火墙。
- 出站事件使用版本化、最小化 DTO;不要把 ORM entity、内部错误、邮箱、token 或整行数据库对象直接 emit。
6. 顺序不等于可靠送达
Socket.IO 保证同一连接上的消息顺序,但默认送达语义是 at-most-once:断线时事件可能没有到达,对端离线期间服务器也不会自动替它保存全部消息。重连成功不意味着业务事实自动补齐。
对可重试命令,使用客户端生成的 idempotency key,并在数据库唯一约束/事务中记录处理结果;重复命令返回同一结果,不重复扣款、创建或发送通知。对需要补发的服务端事件,为事件分配持久 ID,提交业务状态和 outbox 记录,再由 worker 投递;客户端提交最后确认的 offset,服务端按权限从持久事件流补齐。事件流应有保留期限和敏感字段策略。
ACK 超时和重试可以改善客户端到服务端的投递概率,但刷新页面时的内存待发队列会消失;它不能替代服务端幂等和持久化。Connection state recovery 也只是有限窗口内恢复某些会话状态/数据包的能力,不是数据库事件日志。恢复中间件是否重跑、适配器是否支持,都要按实际配置验证;涉及权限变动时不要为了恢复而跳过鉴权。
7. 从单进程走向多实例
单实例 Socket.IO 默认 adapter 的 room 状态在当前进程内。扩展到多个 Node 进程/容器后,同一用户连接可能落在不同实例,广播需要共享 adapter,例如官方 Redis adapter;使用 HTTP long-polling 时通常还需要负载均衡 sticky sessions。只有 WebSocket transport 的拓扑要求不同,但会牺牲 polling 回退能力,必须有明确兼容性评估。
Redis adapter 主要负责跨实例广播,不是可靠消息数据库;消息是否持久、断线补发、幂等和审计仍需应用自己的持久化设计。也要核对 adapter 对 connection-state recovery 的支持表,不要假设切换 Redis adapter 后所有恢复能力仍可用。Redis 需私网访问、ACL、独立最小权限凭据与 TLS(按部署能力),不能对公网开放。
反向代理需正确支持 Upgrade/Connection 头并设置合理的空闲超时;超时太短会制造重连风暴,太长会让失效会话占用资源。发布时支持新旧客户端协议的短期兼容,部署过程中平滑断开,客户端采用有上限的指数退避并加入随机抖动。
8. 生产验收清单
- 握手:无 token、过期 token、撤销 token、停用账号和跨站 Origin 均有确定结果;日志中没有凭据。
- 授权:未加入租户/订单的用户不能订阅、接收或修改该资源;角色变化后旧连接无法继续操作。
- 输入:错误 schema、超长内容、未知事件、突发连接、消息洪峰和慢消费者有受控处理与指标。
- 可靠性:断网重连、重复命令、ACK 超时、服务重启和 outbox 重投不会造成重复业务副作用;遗漏事件可按权限补拉。
- 横向扩展:至少两个服务实例验证 room 广播、会话粘滞策略、adapter 故障以及滚动发布。
- 观测:连接数、握手拒绝原因类别、事件速率、ACK 延迟、队列深度、断连原因和 adapter 错误可观测;标签不能使用 user ID、token 或完整 room 名等高基数/敏感值。
官方资料
- NestJS Gateways:Gateway、异步 handler 与生命周期。
- NestJS WebSocket Guards 与 Pipes:事件授权、输入验证和异常语义。
- Socket.IO middlewares:连接认证和握手 auth。
- Socket.IO delivery guarantees:消息顺序、at-most-once 与补发设计。
- Socket.IO CORS handling:轮询 CORS 与 WebSocket 的区别。
- Socket.IO rooms 和 Redis adapter:房间广播与多节点拓扑。
- Socket.IO server options:连接状态恢复选项及边界。
版本快照:官方资料按 2026-10-14 核对。NestJS/Socket.IO API 会随主版本变化;升级或更换 adapter 后,应重新核对兼容表并跑多实例与恢复测试。
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。