WebSocket连接数限制需在握手阶段用limit_conn按真实IP限流,因后续长连接不计数;必须用map从X-Forwarded-For提取最左公网IP作为key,并配合set_real_ip_from等机制防伪造,否则CDN后所有请求会被误判为同一IP。

直接在 Nginx 层用 limit_conn 限制单个 IP 的 WebSocket 连接数最有效,但必须配合 map 提取真实客户端 IP,否则 CDN 或反向代理后所有连接都会被识别为同一个 IP。
为什么 WebSocket 连接数限制不能照搬 HTTP 配置
WebSocket 升级请求(Upgrade: websocket)本质是 HTTP 请求,Nginx 默认会把它当作普通请求处理;但一旦握手完成,后续通信就走长连接,limit_conn 对已建立的连接不计数——它只管“新建连接”的并发数。所以防护关键点在握手阶段。
- 若未正确提取真实 IP(比如只用
$remote_addr),所有经 CDN 的请求都算作 CDN 节点 IP,导致误封 - HTTP/2 下每个 WebSocket 流可能被视作独立连接,而
limit_conn按 TCP 连接计数,实际效果比预期宽松 -
limit_req(限速)对 WebSocket 握手有效,但无法阻止恶意用户建 100 个长连接后静默占用资源
如何用 map + limit_conn 正确限制真实 IP 的 WebSocket 并发数
核心是把 X-Forwarded-For 头里最左边的非私有 IP 提取出来,作为限流 key。需在 http{} 块中定义:
map $http_x_forwarded_for $real_ip {
~^(\d+\.\d+\.\d+\.\d+) $1;
default $remote_addr;
}
limit_conn_zone $real_ip zone=wsip:10m;然后在 location 中启用(注意必须匹配 WebSocket 升级路径):
location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
limit_conn wsip 5; # 每个真实 IP 最多 5 个 WebSocket 握手请求/秒
limit_conn_status 429;
}-
map规则要放在limit_conn_zone之前,否则变量不可用 - 不要用
$binary_remote_addr直接替换$real_ip,二进制格式不支持从 header 动态解析 - 阈值设为 3–8 较合理:前端单页应用通常 1–2 个连接,设太高失去防护意义,设太低易触发误限
客户端 IP 伪造风险怎么防
仅靠 X-Forwarded-For 不可靠,攻击者可手动构造该 header。必须确保只有可信代理能传入该字段:
- 在 Nginx 前置层(如 CDN 或负载均衡)配置:只允许指定 IP 段(如
set_real_ip_from 192.168.0.0/16;)设置X-Forwarded-For - 配合
real_ip_header X-Forwarded-For;和real_ip_recursive on;,让 Nginx 信任并解析该 header - 若服务直面公网且无 CDN,可跳过
map,直接用$binary_remote_addr—— 更简单也更安全
验证是否生效及常见失败点
真正起作用的是握手请求(GET /ws/ + Upgrade: websocket)被拒时返回 429 Too Many Requests,而不是连接建立后断开。
- 用
curl -i -H "Connection: upgrade" -H "Upgrade: websocket" http://your.site/ws/多次并发测试,观察响应状态码 - 检查 Nginx error log 是否出现
limiting connections by zone "wsip"日志(需配置limit_conn_log_level error) - 如果始终返回 503 或没日志,大概率是
map变量未被识别(检查是否漏写http{}包裹,或变量名拼错) - 宝塔面板用户注意:其自动生成的配置常把
map写在server{}内,必须手动剪切到http{}块顶部
最关键的不是阈值数字,而是确保你限制的是真实用户 IP 而非中间节点——这一步出错,整个防护就形同虚设。


















