一、WebSocket在NestJS中鉴权时容易踩的坑
很多开发者用NestJS做WebSocket网关的时候,第一反应是在连接时做个鉴权,比如检查有没有传token,但容易忽略几个关键的安全点,就像开公司大门只查一次工牌,员工走到办公室后门就不管了,结果恶意人员混了进来。
1.1 只做一次握手鉴权,不跟进后续连接状态
举个例子,在握手时验证了token是对的,允许用户连进来,但这个token可能10分钟后就过期了,用户还在保持连接,网关不会再主动校验,就会产生无效连接,占用服务器资源。很多人忘了,握手只是建立连接的起点,后续的所有数据交互也需要确保合法性。
1.2 握手时没过滤非法请求来源
比如,别人故意模拟大量的WebSocket握手请求,打满服务器的连接池,让合法用户连不上,这是DOS攻击的一种。如果在握手阶段只查token,不检查请求的来源IP、请求频率,就很容易被这种攻击盯上。
1.3 凭证传递方式不安全
很多人把token放在握手请求的URL参数里,抓包工具能直接看到,就像把工牌挂在胸口,路过的人都能看到。如果别人拿到你的token,就能用你的身份连进WebSocket网关,做非法操作。
二、握手阶段安全校验与心跳保活的协同逻辑
这两个机制就像公司的"进门检查"和"保安巡逻",进门查一次工牌,保安每隔一段时间巡逻确认里面的人还在,两个配合才能保证连接既安全又稳定,不会随便掉线。
2.1 握手阶段的安全校验怎么做
在NestJS里,我们可以用网关的handleConnection方法,在建立连接的瞬间做校验,关键是:① 凭证要放在安全的地方(比如请求头,而不是URL参数);② 不仅要验证凭证格式对不对,还要看有没有过期、是不是合法签发的;③ 额外过滤异常请求,比如短时间内握手太频繁的IP直接处理。下面是完整的代码示例,技术栈为NestJS 10.x、@nestjs/websockets、@nestjs/platform-ws、jsonwebtoken:
import { WebSocketGateway, OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect, WebSocketServer } from '@nestjs/websockets';
import { Server, WebSocket } from 'ws';
import { JwtService } from '@nestjs/jwt';
// WebSocket网关,监听8080端口的WS连接
@WebSocketGateway(8080, { transports: ['websocket'], cors: { origin: '*' } })
export class WsGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer() server: Server;
// 存每个客户端的心跳定时器,方便后续清理(避免内存泄漏)
private heartbeatIntervals = new Map<string, NodeJS.Timeout>();
// 存客户端最后心跳时间,用来判断是否失联
private clientLastHeartbeat = new Map<string, number>();
constructor(private readonly jwtService: JwtService) {}
// 网关启动时的初始化操作
afterInit(server: Server) {
console.log('WebSocket网关启动,监听8080端口');
// 全局定时检查所有客户端的心跳,每30秒执行一次
setInterval(() => this.checkHeartbeat(), 30000);
}
// 握手阶段:客户端连接时的核心安全校验
async handleConnection(client: WebSocket & { handhsake: any }) {
// 从握手请求的query里拿token(实际项目要改从请求头取,更安全)
const token = client.handhsake.query?.token;
if (!token) {
client.close(401, '缺少认证凭证'); // 没token直接断开,拒绝连接
return;
}
try {
// 验证token的有效性:是否过期、是不是合法签发的
const user = await this.jwtService.verifyAsync(token, { secret: 'your-secret-key' });
// 把用户id挂在客户端对象上,方便后续识别
client['userId'] = user.sub;
// 初始化该客户端的心跳时间(当前时间戳)
this.clientLastHeartbeat.set(user.sub, Date.now());
// 给每个合法客户端设置心跳定时器:每15秒发一次ping信号
const interval = setInterval(() => {
client.send(JSON.stringify({ type: 'ping' }));
}, 15000);
this.heartbeatIntervals.set(user.sub, interval);
} catch (err) {
client.close(401, '凭证无效或已过期'); // token失效,断开连接
return;
}
console.log(`合法用户 ${client['userId']} 已连接`);
}
// 处理客户端发来的消息(包含心跳响应)
handleMessage(client: WebSocket, message: string) {
const data = JSON.parse(message);
const userId = client['userId'];
if (!userId) return;
// 如果收到的是心跳响应(约定叫"pong"),更新该客户端的最后心跳时间
if (data.type === 'pong') {
this.clientLastHeartbeat.set(userId, Date.now());
}
// 此处可以添加业务消息处理逻辑,比如聊天消息、数据上报等,示例省略
}
// 客户端断开连接时的清理操作(避免内存泄漏)
handleDisconnect(client: WebSocket) {
const userId = client['userId'];
if (userId) {
// 清除该客户端的心跳定时器
const interval = this.heartbeatIntervals.get(userId);
if (interval) clearInterval(interval);
// 清理对应的记录
this.heartbeatIntervals.delete(userId);
this.clientLastHeartbeat.delete(userId);
console.log(`用户 ${userId} 已断开连接`);
}
}
// 全局检查所有客户端的心跳状态,判断是否失联
private checkHeartbeat() {
const now = Date.now();
const maxHeartbeatTimeout = 30000; // 超过30秒没响应就认为失联
for (const [userId, lastTime] of this.clientLastHeartbeat) {
if (now - lastTime > maxHeartbeatTimeout) {
// 找到对应的客户端实例,主动断开连接
const client = Array.from(this.server.clients).find(c => c['userId'] === userId);
if (client) {
client.close(1008, '心跳超时,连接断开');
}
}
}
}
}
2.2 心跳保活机制怎么设计
心跳的核心是"双向确认":网关每隔一段时间给客户端发一个"ping"信号,客户端收到后马上回一个"pong"信号,就像保安每隔5分钟敲你的办公室门,你应声就是还在。如果客户端超过一定时间没回,网关就认为连接断了,主动断开,避免浪费资源。刚才的代码里,网关每15秒发一次ping,全局每30秒检查一次,这个时间设置是经过权衡的:太短会让服务器白忙活,太长会导致真的掉线后还要等很久才清理。
2.3 两者协同的关键点
握手校验是"准入门槛",心跳是"后续维护",两者要配合:① 握手时把合法的客户端标识(比如用户id)和凭证状态存下来,心跳检查时就能快速确认;② 心跳的时候除了响应ping,还可以主动检查当前凭证有没有过期,一旦发现过期,马上断开,不需要等下一次握手;③ 心跳清理的时候,要把对应的内存资源清掉,比如代码里的定时器和心跳时间记录,不然会越积越多,占满服务器内存。
三、实际应用场景与技术利弊
3.1 常见应用场景
这个组合机制特别适合需要实时交互的场景:比如在线聊天软件,用户要发消息,必须保证连接是合法的,而且不会随便掉线;比如实时数据监控,设备要把数据上报到服务器,稳定的长连接才能保证数据不丢;还有在线游戏,玩家要实时操作,连接一断就会掉出游戏,所以心跳和鉴权是必备的。
3.2 技术优缺点
优点:用NestJS做WebSocket网关,框架已经帮你处理了很多底层的连接细节,比如断开后的清理、多客户端的管理,你只需要关注业务逻辑;握手和心跳结合,既保证了安全,又减少了无效连接的占用,服务器能扛更多的连接。缺点:如果心跳时间设置不合理,比如太短,会增加服务器的压力,太长又容易出现假掉线(比如网络临时波动,没有回pong,就被断开);另外,NestJS的WebSocket默认配置如果不限制跨域,容易被恶意网站调用,这也是需要注意的。
3.3 关键注意事项
第一,凭证传递要安全,不要放URL参数,应该用请求头或者在连接后的第一条业务消息里传;第二,心跳的时间要根据实际情况调整,比如移动端的网络波动大,心跳时间可以设短一点,PC端可以长一点;第三,一定要处理客户端的重连逻辑,比如客户端断开后,自动尝试重新连接,并且用新的token;第四,不要忽略内存泄漏,每个客户端的定时器和记录都要在断开时删掉,不然服务器跑久了会变慢。
四、总结
WebSocket网关在NestJS里做鉴权,最容易遗漏的就是"只查一次握手,不跟进后续状态",而握手校验和心跳保活的配合,就是解决这个问题的核心。简单来说,握手是把好入口,心跳是守住后续的连接状态,两者一起,既能保证只有合法用户能连进来,又能让连接稳定不中断,不会被无效连接拖垮服务器。实际项目里,一定要把这两个机制结合起来,注意细节的配置,比如凭证安全、心跳时间、内存清理,这样才能写出稳定又安全的长连接服务。
评论
围绕“WebSocket网关在NestJS中做鉴权时容易遗漏什么?握手阶段的安全校验与心跳保活机制如何协同工作,保证长连接稳定不中断”参与讨论