WebSocket连接认证与消息过滤是网站漏洞防护中最容易被忽视、却又最致命的环节。很多开发者在实现WebSocket通信时,只关注了"能连上、能发消息",却忽略了三个核心问题:谁能连接、连接后能干什么、消息内容是否安全。这三个问题如果不解决,攻击者可以绕过传统的HTTP防护机制,直接通过WebSocket通道注入恶意数据、窃取敏感信息、甚至接管用户会话。下面我会从认证机制、消息过滤、实战防护三个层面,把这套防护体系讲透。

一、WebSocket为什么比HTTP更危险

传统HTTP请求是"请求-响应"模式,每次通信都是独立的,服务器可以在每次请求时做身份校验、参数过滤。但WebSocket是全双工长连接,一旦握手成功,后续通信就像一条不设关卡的管道。如果握手阶段的认证做得不到位,或者连接建立后没有持续的消息过滤,攻击者就能利用这条管道做很多事情:发送跨站WebSocket劫持(CSWSH)攻击、注入恶意脚本、绕过CSRF防护、甚至通过大量无效连接耗尽服务器资源。

更关键的是,很多Web应用的安全策略只覆盖了HTTP层,防火墙、WAF、速率限制等机制对WebSocket流量往往是"放行"状态。这就意味着WebSocket成了整个防护体系中的盲区。所以,WebSocket的安全防护必须从应用层自己做起,不能依赖外部设备。

二、WebSocket连接认证的四层防护体系

认证是第一道门,必须做扎实。我建议采用四层认证机制,层层递进,缺一不可。

第一层:握手阶段的Token验证

WebSocket握手本质上是一个HTTP升级请求(Upgrade: websocket),这个阶段可以而且必须做身份验证。最常见的做法是在握手请求的URL中携带Token,或者在Sec-WebSocket-Protocol头中传递认证信息。服务器在收到Upgrade请求时,先解析Token,验证其有效性和过期时间,验证通过才返回101状态码完成握手。

// Node.js 示例:WebSocket握手阶段Token验证
const WebSocket = require('ws');
const jwt = require('jsonwebtoken');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('headers', (headers, req) => {
    const token = new URL(req.url, 'http://localhost').searchParams.get('token');
    if (!token) {
        return new Response('Unauthorized', { status: 401 });
    }
    try {
        const decoded = jwt.verify(token, 'your-secret-key');
        req.user = decoded; // 将用户信息挂载到请求对象
    } catch (err) {
        return new Response('Invalid token', { status: 401 });
    }
});

wss.on('connection', (ws, req) => {
    console.log(`User ${req.user.id} connected`);
    // 后续消息处理
});

第二层:Origin来源校验

除了Token,还必须校验Origin头。WebSocket握手请求会携带Origin字段,标明请求来源。服务器应该维护一个白名单,只允许来自可信域名的连接。这一步能有效防止跨站WebSocket劫持攻击。注意,Origin校验不能替代Token验证,两者要配合使用。

// Origin白名单校验
const allowedOrigins = ['https://yourdomain.com', 'https://app.yourdomain.com'];

wss.on('headers', (headers, req) => {
    const origin = headers.origin;
    if (origin && !allowedOrigins.includes(origin)) {
        return new Response('Forbidden origin', { status: 403 });
    }
});

第三层:连接频率与并发限制

即使认证通过了,也要限制单个用户或单个IP的连接数量和频率。一个正常用户不会同时建立几十个WebSocket连接,如果出现这种情况,大概率是攻击行为。可以用Redis记录每个用户的活跃连接数,超过阈值就拒绝新连接。

// 基于Redis的连接数限制
const Redis = require('ioredis');
const redis = new Redis();

async function checkConnectionLimit(userId) {
    const key = `ws:conn:${userId}`;
    const count = await redis.incr(key);
    if (count === 1) {
        await redis.expire(key, 3600); // 1小时过期
    }
    return count  {
    if (!await checkConnectionLimit(req.user.id)) {
        ws.close(4001, 'Too many connections');
        return;
    }
    // 正常处理
});

第四层:持续会话验证

Token验证只在握手时做一次是不够的。Token可能在连接期间过期,用户可能在其他设备登出导致Token失效。所以需要一个心跳机制或者定期重验证机制。比如每隔5分钟检查一次Token状态,或者在用户执行敏感操作时重新验证。

三、WebSocket消息过滤的核心策略

认证解决了"谁能连"的问题,消息过滤解决的是"连了之后能发什么"的问题。这是很多项目最薄弱的环节。

1. 输入验证:不信任任何客户端数据

所有从客户端发来的消息,都必须当作不可信数据处理。验证消息格式是否符合预期的JSON结构,字段类型是否正确,长度是否在合理范围内。不要假设客户端会按照协议发数据,攻击者会故意构造畸形消息来探测漏洞。

// 消息格式验证示例
function validateMessage(data) {
    const schema = {
        type: 'string',
        maxLength: 50,
        payload: 'object',
        payloadKeys: ['content', 'timestamp']
    };
    
    if (typeof data !== 'object' || data === null) {
        return { valid: false, reason: 'Invalid format' };
    }
    if (typeof data.type !== 'string' || data.type.length > schema.type.maxLength) {
        return { valid: false, reason: 'Invalid type field' };
    }
    if (typeof data.payload !== 'object') {
        return { valid: false, reason: 'Invalid payload' };
    }
    return { valid: true };
}

2. 内容过滤:防止注入攻击

WebSocket消息中如果包含用户输入的文本内容,必须做XSS过滤和SQL注入防护。虽然WebSocket不直接操作数据库,但消息内容最终可能被存入数据库或展示给其他用户。对消息内容进行HTML实体编码、特殊字符转义是基本操作。同时要警惕JSON注入,攻击者可能在消息中嵌入额外的JSON结构来篡改数据。

3. 消息大小限制

设置单条消息的最大字节数,通常建议不超过64KB。超大消息不仅可能是攻击(比如内存耗尽攻击),也会影响其他用户的通信质量。在服务器端配置maxPayload参数,超过限制直接断开连接。

// 设置最大消息大小
const wss = new WebSocket.Server({ 
    port: 8080,
    maxPayload: 65536 // 64KB
});

4. 消息频率限制

单个连接在单位时间内发送的消息数量也要限制。可以用令牌桶算法实现,比如每秒最多20条消息,超过就暂时拒绝并发出警告。这能有效防止刷屏攻击和自动化攻击工具的高频探测。

5. 敏感操作二次确认

对于涉及资金、权限变更、数据删除等敏感操作的消息,不能仅凭一条WebSocket消息就执行。需要在消息中携带二次验证因子,比如短信验证码、生物识别确认等,或者要求客户端重新输入密码。这是纵深防御的重要一环。

四、实战中容易踩的五个坑

坑一:用Cookie做WebSocket认证

很多人觉得WebSocket握手是HTTP请求,直接用Cookie里的SessionID就行。问题是,Cookie容易被CSRF攻击利用,而且在某些跨域场景下Cookie不会自动携带。建议用专门的Token机制,不要复用HTTP Session。

坑二:忽略WSS(加密传输)

如果你的网站是HTTPS的,WebSocket必须用WSS协议。用WS明文传输等于把所有通信内容暴露在网络中,中间人可以轻易篡改消息内容。配置SSL证书并强制WSS连接是基本要求。

坑三:没有日志和监控

WebSocket连接是长连接,出了问题很难排查。必须记录每个连接的建立时间、用户ID、消息数量、异常断开原因。配合实时监控告警,当出现异常连接模式时能第一时间响应。

坑四:认为内网WebSocket不需要防护

很多人觉得WebSocket只对外暴露,内网服务之间的WebSocket通信不需要认证。这是错误的。内网一旦被突破,没有认证的WebSocket就是攻击者横向移动的高速公路。内网通信同样需要mTLS双向认证。

坑五:过度信任前端过滤

前端做的任何输入验证都可以被绕过。攻击者可以直接用工具构造WebSocket消息发送到服务端。所有安全校验必须在服务端完成,前端验证只是提升用户体验,不是安全措施。

五、一套完整的防护架构建议

把上面讲的内容整合起来,一套生产级的WebSocket安全防护架构应该包含:握手阶段的Token+Origin双重验证、连接数和频率限制、消息格式校验+内容过滤+大小限制、WSS加密传输、持续会话监控、完整的审计日志、异常自动熔断机制。这不是某一个技术点能解决的,而是一个体系。

从开发角度来说,建议把WebSocket的认证和过滤逻辑封装成中间件或独立模块,不要散落在业务代码里。这样既方便复用,也方便后续升级安全策略。同时定期做安全审计和渗透测试,特别是针对WebSocket通道的专项测试,很多通用扫描工具是检测不到WebSocket漏洞的。

最后说一句,WebSocket安全不是"加了就行",而是要根据业务场景持续调整。聊天应用、实时交易、物联网控制,不同场景的防护重点完全不同。理解业务,才能做对防护。