16. 安全性基础:WebSocket 加密(WSS)、消息校验、防止恶意连接

说实话,信令服务器这东西,很多人一开始都觉得——不就是个消息中转站嘛,能有什么安全问题?

我以前也这么想。直到有一次,我负责的一个WebRTC项目上线第二天,就被别人用脚本疯狂连接,服务器直接打满了。更尴尬的是,有人伪造了信令消息,把别人的视频流给劫走了。嗯,从那以后,我再也不敢轻视信令服务器的安全性了。

这一章,我们就来聊聊信令服务器安全的三板斧:WSS加密消息校验防止恶意连接。说白了,就是让你的信令服务器穿上防弹衣。

16.1 为什么信令服务器需要安全防护?

先想一个问题:信令服务器在WebRTC架构里扮演什么角色?

它是所有客户端交换连接信息的桥梁。SDP、ICE候选者、房间控制指令……这些数据一旦被篡改或泄露,后果很严重。

我遇到过最典型的攻击场景:

  • 中间人攻击:明文WebSocket传输,数据包被截获,SDP被替换
  • 消息伪造:攻击者模拟合法客户端,发送虚假的offer/answer
  • 连接洪泛:大量伪造连接耗尽服务器资源,导致正常用户无法接入

所以,安全不是可选项,是必选项。

核心原则:信令服务器的安全设计,应该遵循“默认拒绝”策略——任何未经验证的连接和消息,一律拒绝。

16.2 WSS:给WebSocket穿上TLS外衣

WSS就是WebSocket over TLS。说白了,就是把WebSocket通信加密了。

我个人习惯,生产环境绝对不用WS,只用WSS。为什么?因为WS是明文传输,抓包工具一抓,你的SDP、ICE候选者全暴露了。

16.2.1 配置WSS服务器

以Node.js的ws库为例,配置WSS其实就多了一步:加载TLS证书。

const fs = require('fs');
const https = require('https');
const WebSocket = require('ws');

// 加载证书
const server = https.createServer({
  cert: fs.readFileSync('/path/to/cert.pem'),
  key: fs.readFileSync('/path/to/key.pem')
});

const wss = new WebSocket.Server({ server });

wss.on('connection', (ws, req) => {
  console.log('WSS连接已建立');
  // 处理消息...
});

server.listen(443, () => {
  console.log('WSS服务器运行在端口443');
});

这里要注意:证书必须是有效的。我见过有人用自签名证书上线,结果客户端连不上,因为浏览器不信任自签名证书。生产环境建议用Let's Encrypt,免费且自动续期。

小技巧:如果你用Nginx做反向代理,可以在Nginx层终止TLS,然后把明文WebSocket转发到后端。这样后端代码不用改,证书管理也方便。

16.2.2 客户端连接WSS

客户端连接WSS和连接WS几乎一样,只是把ws://换成wss://

const socket = new WebSocket('wss://your-server.com/ws');

socket.onopen = () => {
  console.log('WSS连接成功');
};

socket.onmessage = (event) => {
  console.log('收到消息:', event.data);
};

嗯,就这么简单。但加密只是第一步,接下来我们还要解决消息可信的问题。

16.3 消息校验:确保消息来自合法客户端

WSS解决了“别人能不能偷看”的问题,但没解决“别人能不能冒充”的问题。

举个例子:攻击者虽然不知道你的SDP内容,但他可以伪造一条消息,说“我是用户A,我要和用户B通话”。如果服务器不校验,这条假消息就会被转发给用户B。

我曾经在调试一个项目时,发现有人用Postman直接往我的WebSocket发消息……嗯,当时我就意识到,必须加消息校验。

16.3.1 基于Token的校验

最常用的方案是:客户端连接时携带一个Token,服务器验证Token有效后才允许通信。

// 服务器端:验证Token
wss.on('connection', (ws, req) => {
  const token = req.url.split('?token=')[1];
  
  if (!token || !verifyToken(token)) {
    ws.close(4001, 'Token无效');
    return;
  }
  
  console.log('合法客户端连接');
  // 继续处理...
});

function verifyToken(token) {
  // 验证逻辑:比如JWT验证
  // 返回true/false
}

客户端连接时,把Token放在URL参数里:

const token = 'your-jwt-token';
const socket = new WebSocket(`wss://your-server.com/ws?token=${token}`);

注意:Token不要放在URL里?其实WSS下URL是加密传输的,所以安全。但Token本身要有过期时间,防止泄露后被长期使用。

16.3.2 消息签名

除了连接时的Token校验,每条消息也可以加签名。这样即使Token泄露,攻击者也无法伪造消息内容。

// 客户端:发送带签名的消息
const message = {
  type: 'offer',
  target: 'userB',
  sdp: '...'
};

const secret = 'shared-secret-key';
const signature = crypto
  .createHmac('sha256', secret)
  .update(JSON.stringify(message))
  .digest('hex');

socket.send(JSON.stringify({
  ...message,
  signature
}));

服务器收到后,验证签名:

// 服务器端:验证消息签名
wss.on('message', (data) => {
  const msg = JSON.parse(data);
  const { signature, ...payload } = msg;
  
  const expectedSig = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');
  
  if (signature !== expectedSig) {
    ws.close(4002, '消息签名无效');
    return;
  }
  
  // 签名有效,处理消息
});

我个人建议:Token校验 + 消息签名一起用。Token保证连接合法,签名保证消息未被篡改。

16.4 防止恶意连接:别让服务器被拖垮

校验做好了,但还有一个问题:攻击者可以发起大量连接,即使每个连接都带Token,服务器也扛不住。

我记得有一次线上事故,就是被人用脚本开了几千个WebSocket连接,服务器CPU直接飙到100%。从那以后,我加了三道防线。

16.4.1 连接频率限制

限制每个IP在一定时间内的连接次数。

const rateLimit = new Map();

wss.on('connection', (ws, req) => {
  const ip = req.socket.remoteAddress;
  const now = Date.now();
  
  if (!rateLimit.has(ip)) {
    rateLimit.set(ip, []);
  }
  
  const timestamps = rateLimit.get(ip);
  const recent = timestamps.filter(t => now - t < 60000); // 1分钟内
  
  if (recent.length >= 10) { // 每分钟最多10次连接
    ws.close(4003, '连接过于频繁');
    return;
  }
  
  recent.push(now);
  rateLimit.set(ip, recent);
  
  // 继续处理...
});

16.4.2 最大连接数限制

设置服务器全局最大连接数,防止资源耗尽。

const MAX_CONNECTIONS = 1000;
let currentConnections = 0;

wss.on('connection', (ws, req) => {
  if (currentConnections >= MAX_CONNECTIONS) {
    ws.close(4004, '服务器繁忙');
    return;
  }
  
  currentConnections++;
  
  ws.on('close', () => {
    currentConnections--;
  });
  
  // 继续处理...
});

16.4.3 心跳检测

有些恶意连接建立后不发送任何数据,就占着连接不释放。心跳检测可以及时清理这些僵尸连接。

const HEARTBEAT_INTERVAL = 30000; // 30秒

wss.on('connection', (ws) => {
  ws.isAlive = true;
  
  ws.on('pong', () => {
    ws.isAlive = true;
  });
  
  const interval = setInterval(() => {
    if (!ws.isAlive) {
      ws.terminate();
      clearInterval(interval);
      return;
    }
    
    ws.isAlive = false;
    ws.ping();
  }, HEARTBEAT_INTERVAL);
  
  ws.on('close', () => {
    clearInterval(interval);
  });
});

避坑指南:我曾经把心跳间隔设得太短(5秒),结果正常用户因为网络波动被频繁踢下线。建议30秒到60秒比较合理。

16.5 知识体系总览

下面这张图,把信令服务器安全的核心逻辑串起来了:

信令服务器安全架构 客户端 WSS加密传输 TLS 1.2/1.3 连接控制 频率限制 最大连接数 消息校验 Token验证 消息签名 信令业务处理 拒绝连接 拒绝消息 任何一层校验失败 立即断开连接

从这张图可以看得很清楚:客户端发起的连接,要经过WSS加密、连接控制、消息校验三层过滤,才能到达业务处理层。任何一层出问题,直接拒绝。

16.6 总结

信令服务器的安全,说白了就是三件事:

  • 加密通信:用WSS代替WS,防止数据被窃听
  • 校验身份:Token验证 + 消息签名,防止冒充和篡改
  • 限制资源:频率限制、最大连接数、心跳检测,防止被拖垮

这三板斧用好了,你的信令服务器基本就稳了。当然,安全是一个持续对抗的过程,没有绝对的安全。但至少,别让攻击者太轻松。

嗯,这一章就到这里。记住:安全设计要前置,别等出事了再补。

一句话总结:WSS保加密,Token保身份,限流保资源——信令服务器安全三件套,缺一不可。


公众号:蓝海资料掘金营,微信deep3321