websocket黑名单必须作用于upgrade请求,因为握手阶段是唯一能拦截http协议栈的时机;一旦返回101状态完成协议切换,后续通信脱离http,应用层中间件将无法感知或阻断连接。

WebSocket 黑名单机制不能只靠前端或应用层单点拦截,必须在握手阶段(HTTP Upgrade 请求)就生效,否则连接已建立,封禁就晚了。
为什么 WebSocket 黑名单必须作用于 Upgrade 请求
WebSocket 连接建立分两步:先发一个带 Upgrade: websocket 和 Connection: Upgrade 的 HTTP 请求(即握手),服务端返回 101 Switching Protocols 后才真正切换协议。一旦切换完成,后续帧通信不再走 HTTP 协议栈,传统 Web 中间件(如 Spring Security 的 HttpSecurity)或路由层就完全“看不见”消息了。
常见错误是把黑名单逻辑写在 onOpen 回调里——此时连接已建立,内存、文件描述符、会话上下文都已分配,攻击者早就能发起空闲连接或高频 ping 消耗资源。
- 黑名单检查必须在请求进入业务逻辑前完成,理想位置是反向代理层或 WebSocket 服务器的握手预处理钩子
- IP 提取要防伪造:Nginx 需配置
proxy_set_header X-Real-IP $remote_addr,后端必须从X-Real-IP或X-Forwarded-For(取第一个非私有地址)读取,而非直接用$remote_addr - 黑名单数据结构推荐用布隆过滤器(Bloom Filter)+ Redis TTL,避免每次查库;若用内存 Map,需注意多进程/多实例间的同步问题
Ratchet 中用 IpBlackList 装饰器拦截非法 IP
Ratchet 的 IpBlackList 是专为握手阶段设计的装饰器,它包装 WsServer,在 onOpen 触发前就校验客户端 IP。
使用时注意三点:
- 必须把
IpBlackList实例注册到App::route(),而不是原始组件:$app->route('/ws', $ipBlackListInstance),否则装饰器不生效 - 黑名单列表支持 CIDR 表示法,例如
['192.168.1.0/24', '203.0.113.42'],但不自动解析 DNS 或动态更新,需自行 reload - 被拒绝的请求会返回
403 Forbidden,且不触发onClose—— 因为连接根本没建立,这点和业务层抛异常不同
示例代码片段:
use Ratchet\Server\IpBlackList;
use Ratchet\WebSocket\WsServer;
$component = new MyChatComponent();
$wsServer = new WsServer($component);
$blackList = new IpBlackList($wsServer, ['198.51.100.0/24', '203.0.113.12']);
$app = new Ratchet\App('localhost', 8080);
$app->route('/ws', $blackList); // 注意:这里不是 $component
$app->run();
Nginx 层做连接级 IP 封禁更高效
比起应用层拦截,Nginx 在四层/七层之间做限流和封禁更轻量、更早、也更抗压。它能在 TCP 握手后、HTTP 解析前就丢弃恶意连接。
关键配置项:
-
limit_conn_zone $binary_remote_addr zone=ws_limit:10m;—— 按客户端 IP 建立连接数限制区 -
limit_conn ws_limit 5;—— 单 IP 最大并发 WebSocket 连接数(配合proxy_http_version 1.1使用) - 搭配
map提取真实 IP:map $http_x_forwarded_for $client_real_ip { ... },再基于$client_real_ip限流 - 用
deny+allow直接拒绝 IP:deny 203.0.113.0/24; allow all;,但注意该指令仅对 HTTP 请求有效,对 WebSocket 握手也适用,因握手仍是 HTTP 请求
特别提醒:deny/allow 在 location 块中生效,必须确保 WebSocket 路径(如 /ws)的 location 块里显式写了这些规则,否则会被上级配置覆盖。
Swoole 中 onRequest 阶段拦截最灵活
Swoole 的 onRequest 回调发生在 HTTP 请求解析完成后、路由分发前,是插入黑名单逻辑的黄金位置——它比 Ratchet 的装饰器更底层,也比 Spring Security 的 Filter 更早。
实操要点:
- 不要在
onRequest里做耗时操作(如 MySQL 查询),应使用协程 + Redis 异步查黑名单,避免阻塞事件循环 - 封禁响应必须明确返回
403并调用$response->end(),否则 Swoole 会继续向下执行,可能误入路由逻辑 - 可结合
server->getClientInfo($request->fd)获取客户端 IP 和连接时间,用于判断是否为短连风暴(如 1 秒内新建 10 个连接) - 记录日志时带上
$request->header['user-agent']和$request->server['request_time_float'],便于事后溯源攻击特征
容易忽略的是:Swoole 默认不解析 X-Forwarded-For,需手动从 header 取值并验证其合法性(比如检查是否含多个 IP、是否为私有地址),否则攻击者可通过伪造 header 绕过封禁。











