Nginx限流与DDoS防护

2 分钟阅读·Matthieu·fail2banddos-protectionrate-limitingsecuritynginx|

通过配置Nginx的limit_req、limit_conn和fail2ban来保护服务器免受暴力破解和应用层DDoS攻击,无需依赖第三方服务。

限流(rate limiting)是抵御暴力破解攻击、API滥用和应用层DDoS的第一道防线。本教程仅使用Nginx和fail2ban构建三层防护。不需要第三方DDoS服务,流量不会离开你的服务器。

你将在Debian 12或Ubuntu 24.04上配置请求限流(limit_req)、连接节流(limit_conn)和自动IP封禁(fail2ban)。

前提条件:

  • 已安装并运行Nginx
  • 熟悉Nginx配置文件结构
  • 拥有root或sudo权限

Nginx限流的工作原理

Nginx限流使用漏桶算法(leaky bucket),通过limit_req_zonelimit_req两个指令实现。传入请求以任意速率填充桶。桶以你定义的固定速率排出。当桶溢出时,Nginx拒绝多余的请求。这样既能平滑流量尖峰,又能保持稳定的处理速率。

实现涉及两个指令。limit_req_zone定义共享内存区域,用于跨所有worker进程追踪客户端状态。limit_req将限制应用到特定的location。

# 在http块中:定义zone
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

# 在server或location块中:应用限制
limit_req zone=api;

键值$binary_remote_addr以紧凑的二进制格式存储每个客户端IP(IPv4占4字节,IPv6占16字节)。10 MB的zone大约能容纳160,000个IPv4地址或80,000个IPv6地址。对于大多数服务器,10m足够了。

rate参数接受每秒请求数(r/s)或每分钟请求数(r/m)。Nginx在内部以毫秒为单位追踪。10r/s意味着每100ms允许一个请求。

如何配置limit_req_zone和limit_req?

创建一个独立的限流配置文件,保持模块化:

sudo nano /etc/nginx/conf.d/rate-limiting.conf
# 共享内存区域 - 在http级别定义
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;

# 返回429而不是默认的503
limit_req_status 429;

# 在warn级别记录限流事件(延迟在notice级别记录)
limit_req_log_level warn;

然后在server块中应用这些zone:

sudo nano /etc/nginx/sites-available/example.com
server {
    listen 80;
    server_name example.com;

    # 所有请求的通用限流
    limit_req zone=general burst=20 nodelay;

    location /login {
        # 登录端点的严格限制
        limit_req zone=login burst=3 nodelay;
        proxy_pass http://127.0.0.1:3000;
    }

    location /api/ {
        # API消费者的更高限制
        limit_req zone=api burst=50 delay=30;
        proxy_pass http://127.0.0.1:3000;
    }

    location / {
        proxy_pass http://127.0.0.1:3000;
    }
}

测试配置并重载:

sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
sudo systemctl reload nginx

location块中定义了limit_req指令时,它会覆盖从server级别继承的任何limit_reqgeneral zone适用于/,但不适用于/login/api/,因为这些location有自己的limit_req指令。如果需要两个zone同时生效,在同一个块中添加多行limit_req

burst、nodelay和delay的作用

burst参数控制Nginx将多少超额请求排队而不是直接拒绝。没有burst时,任何超出速率的请求都会收到429。有burst时,Nginx将超额请求保存在队列中,以基础速率释放。

参数 立即处理 排队请求 拒绝 使用场景
burst=0(默认) 每间隔1个 超出速率的所有请求 严格的API限制
burst=5 每间隔1个 最多5个,以基础速率释放 超过burst+1 表单提交
burst=5 nodelay 最多同时6个 无排队,但burst槽位以基础速率恢复 超过burst+1直到槽位恢复 登录页面、一般流量
burst=20 delay=10 最多同时11个 第12-21个请求以基础速率节流 超过burst+1 有偶发尖峰的API

使用burst=5(无nodelay)时,如果6个请求同时到达,请求1立即处理。请求2-6排队,每间隔释放一个(10r/s时每100ms一个)。最后一个排队请求等待500ms。这增加了延迟,但不会丢弃合法的突发流量。

使用burst=5 nodelay时,6个请求全部立即处理。但5个burst槽位需要500ms才能恢复。如果200ms后又来了6个请求,只有3个槽位恢复了,所以3个超额请求被拒绝。

使用burst=20 delay=10时,前11个请求(1个基础+10个delay阈值)无需等待即可处理。第12-21个请求以基础速率节流。超过21个的全部拒绝。这种混合模式适合接收合法批量客户端周期性突发的API。

如何对不同端点分别限流?

定义使用不同键的独立zone来应用独立限制。上面的示例已经使用了三个zone。你也可以使用$uri作为键按URI路径限流:

# 按URI限流:限制每个唯一URI的总请求数
limit_req_zone $uri zone=per_uri:10m rate=50r/s;

当某些端点(如搜索页面或导出功能)需要全局节流而不管哪个客户端调用时,这很有用。

对于基于API密钥的限流,使用map从header中提取密钥:

map $http_x_api_key $api_key_limit {
    default          $binary_remote_addr;
    "~^.+$"          $http_x_api_key;
}

limit_req_zone $api_key_limit zone=api_keyed:10m rate=100r/s;

如果客户端发送了X-API-Key header,限流就基于该密钥。否则回退到基于IP的限制。

如何用limit_conn节流连接?

limit_req控制请求速率,而limit_conn限制单个客户端的并发连接数。它对slowloris攻击和下载滥用很有效。

# 在http块中
limit_conn_zone $binary_remote_addr zone=addr:10m;
limit_conn_status 429;
limit_conn_log_level warn;
# 在server或location块中
server {
    # 每个IP最多20个并发连接
    limit_conn addr 20;

    location /downloads/ {
        # 每个IP最多2个并发下载
        limit_conn addr 2;
        limit_rate 1m;  # 同时限制带宽为每连接1MB/s
    }
}

关于HTTP/2和HTTP/3的注意事项:每个并发请求都算作一个独立连接。浏览器通过单个HTTP/2连接加载包含30个资源的页面,对于limit_conn来说算30个连接。设置的限制要比HTTP/1.1高。

limit_connlimit_req互为补充。两者都要用。limit_req阻止快速连发请求。limit_conn阻止连接洪泛。

如何返回自定义429错误页面?

默认情况下,被限流的请求收到一个通用错误页面。自定义429页面可以包含Retry-After header和可读的提示信息。

创建错误页面:

sudo mkdir -p /var/www/error
sudo nano /var/www/error/429.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>429 - Too Many Requests</title>
    <style>
        body { font-family: system-ui, sans-serif; text-align: center; padding: 5rem 1rem; }
        h1 { font-size: 2rem; }
        p { color: #555; }
    </style>
</head>
<body>
    <h1>429 - Too Many Requests</h1>
    <p>You have exceeded the request limit. Wait a moment and try again.</p>
</body>
</html>
sudo chmod 644 /var/www/error/429.html

在server块中添加错误页面和Retry-After header:

server {
    #... 限流指令...

    error_page 429 /429.html;
    location = /429.html {
        root /var/www/error;
        internal;
        add_header Retry-After 5 always;
    }
}

internal指令防止直接访问错误页面。add_header上的always关键字确保即使在错误响应中也发送该header。Retry-After值(单位为秒)告诉行为规范的客户端何时重试。

如何用dry_run安全测试限流?

启用limit_req_dry_run on可以模拟限流而不拒绝请求。Nginx会记录它本来会执行的操作,但所有请求正常通过。此选项从Nginx 1.17.1开始可用。

server {
    limit_req zone=general burst=20 nodelay;
    limit_req_dry_run on;  # 仅记录,不执行

    # 将限流状态添加到访问日志
    #...
}

在日志格式中添加$limit_req_status以在访问日志中追踪dry run事件:

# 在http块中
log_format ratelimit '$remote_addr - $remote_user [$time_local] '
                     '"$request" $status $body_bytes_sent '
                     '"$http_referer" "$http_user_agent" '
                     'rate_limit=$limit_req_status';

# 在server块中
access_log /var/log/nginx/access.log ratelimit;

dry_run工作流程:

  1. 在配置中添加limit_req_dry_run on;
  2. 重载Nginx
  3. 生成测试流量(参见下面的测试部分)
  4. 检查错误日志中的dry run条目:
sudo grep "dry run" /var/log/nginx/error.log
2026/03/19 14:22:31 [warn] 1234#1234: *567 limiting requests, dry run, excess: 1.532 by zone "general", client: 203.0.113.50, server: example.com, request: "GET / HTTP/1.1", host: "example.com"

这里的日志级别是[warn],因为设置了limit_req_log_level warn指令。确保你的error_log指令包含warn级别或更低级别,否则这些消息不会出现。生产配置中的error_log /var/log/nginx/example.error.log warn;已处理此问题。

  1. 检查访问日志中的$limit_req_status变量:
sudo grep "REJECTED_DRY_RUN\|DELAYED_DRY_RUN" /var/log/nginx/access.log

$limit_req_status变量返回以下值之一:PASSEDDELAYEDREJECTEDDELAYED_DRY_RUNREJECTED_DRY_RUN

  1. 当速率看起来合适时,删除limit_req_dry_run行并重载。

如何将可信IP加入白名单?

使用geo块将监控系统、负载均衡器或办公室IP排除在限流之外:

# 在http块中
geo $limit {
    default 1;
    10.0.0.0/8      0;  # 内部网络
    192.168.0.0/16   0;  # 内部网络
    203.0.113.10     0;  # 监控服务器
}

map $limit $limit_key {
    0 "";
    1 $binary_remote_addr;
}

limit_req_zone $limit_key zone=general:10m rate=10r/s;

$limit_key为空字符串时,Nginx完全跳过该请求的限流。匹配geo块的IP得到$limit = 0,映射为空键。

如果你希望白名单IP获得更高速率而非无限制:

limit_req_zone $limit_key zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=trusted:1m rate=100r/s;

server {
    limit_req zone=general burst=20 nodelay;
    limit_req zone=trusted burst=100 nodelay;
}

所有IP都匹配trusted,但只有非白名单IP匹配general。最严格的限制生效,因此白名单IP实际限制为100 r/s,其他人为10 r/s。

如何用fail2ban封禁重复违规者?

限流拒绝单个请求,但持续的攻击者会不断回来。fail2ban监控Nginx错误日志,在多次违规后在防火墙层面封禁IP。

如果尚未安装fail2ban:

sudo apt update && sudo apt install -y fail2ban
sudo systemctl enable --now fail2ban
sudo systemctl status fail2ban
 fail2ban.service - Fail2Ban Service
     Loaded: loaded (/usr/lib/systemd/system/fail2ban.service; enabled; preset: enabled)
     Active: active (running) since Wed 2026-03-19 14:30:00 UTC; 2s ago

fail2ban自带nginx-limit-req过滤器。该过滤器的正则表达式匹配如下行:

limiting requests, excess: 1.532 by zone "general", client: 203.0.113.50

创建jail配置。不要直接编辑.conf文件,使用.local覆盖文件:

sudo nano /etc/fail2ban/jail.local
[nginx-limit-req]
enabled  = true
port     = http,https
filter   = nginx-limit-req
logpath  = /var/log/nginx/error.log
maxretry = 10
findtime = 60
bantime  = 600

这会在60秒内发生10次限流违规后封禁IP 10分钟。

要实现递增封禁,在同一文件中添加第二个jail:

[nginx-limit-req-repeat]
enabled  = true
port     = http,https
filter   = nginx-limit-req
logpath  = /var/log/nginx/error.log
maxretry = 30
findtime = 3600
bantime  = 86400

第一个jail捕获短时突发(1分钟内10次=封禁10分钟)。第二个捕获持续违规者(1小时内30次=封禁24小时)。

重启fail2ban并检查jail状态:

sudo systemctl restart fail2ban
sudo fail2ban-client status nginx-limit-req

在Debian 12和较旧系统上,输出如下:

Status for the jail: nginx-limit-req
|- Filter
|  |- Currently failed:	0
|  |- Total failed:	0
|  `- File list:	/var/log/nginx/error.log
`- Actions
   |- Currently banned:	0
   |- Total banned:	0
   `- Banned IP list:

在Ubuntu 24.04上,fail2ban默认使用systemd journal后端(backend = auto解析为systemd)。输出显示Journal matches:而不是File list:

Status for the jail: nginx-limit-req
|- Filter
|  |- Currently failed:	0
|  |- Total failed:	0
|  `- Journal matches:	_SYSTEMD_UNIT=nginx.service + _COMM=nginx
`- Actions
   |- Currently banned:	0
   |- Total banned:	0
   `- Banned IP list:

两种后端都能正常工作。journal后端通过systemd读取相同的Nginx日志消息。如果你更喜欢基于文件的监控,在jail部分添加backend = pyinotify

测试期间手动解封IP:

sudo fail2ban-client set nginx-limit-req unbanip 203.0.113.50

jail日志位于:

sudo journalctl -u fail2ban -f

如何验证限流是否生效?

从服务器外部的机器测试。不要从localhost测试,因为127.0.0.1可能在你的白名单中。

用curl循环快速测试:

for i in $(seq 1 20); do
    curl -s -o /dev/null -w "%{http_code}\n" https://example.com/
done

速率为10r/sburst=20 nodelay时,前21个请求返回200。burst耗尽后,响应变为429

wrk进行负载测试:

sudo apt install -y wrk
wrk -t2 -c10 -d10s https://example.com/
Running 10s test @ https://example.com/
  2 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     5.23ms    2.11ms  28.44ms   75.32%
    Req/Sec     1.02k   121.33     1.34k    68.00%
  20384 requests in 10.01s, 15.22MB read
  Non-2xx or 3xx responses: 18241
Requests/sec:   2036.36
Transfer/sec:      1.52MB

Non-2xx or 3xx responses计数显示了Nginx限流的请求数。这里20,384个请求中有18,241个收到了429。

测试期间检查错误日志:

sudo tail -f /var/log/nginx/error.log
2026/03/19 14:45:12 [warn] 1234#1234: *890 limiting requests, excess: 9.876 by zone "general", client: 203.0.113.50, server: example.com, request: "GET / HTTP/1.1", host: "example.com"

excess值显示请求超出限制的程度。数值越高表示流量越激进。

完整生产配置

结合三层防护的完整限流配置:

# /etc/nginx/conf.d/rate-limiting.conf

# --- 白名单 ---
geo $limit {
    default 1;
    10.0.0.0/8      0;
    192.168.0.0/16   0;
    # 在此添加你的监控/办公室IP
}

map $limit $limit_key {
    0 "";
    1 $binary_remote_addr;
}

# --- 请求速率zone ---
limit_req_zone $limit_key zone=general:10m rate=10r/s;
limit_req_zone $limit_key zone=login:10m rate=1r/s;
limit_req_zone $limit_key zone=api:10m rate=30r/s;

# --- 连接zone ---
limit_conn_zone $binary_remote_addr zone=addr:10m;

# --- 响应码和日志 ---
limit_req_status 429;
limit_conn_status 429;
limit_req_log_level warn;
limit_conn_log_level warn;

# --- 包含限流状态的访问日志 ---
log_format ratelimit '$remote_addr - $remote_user [$time_local] '
                     '"$request" $status $body_bytes_sent '
                     '"$http_referer" "$http_user_agent" '
                     'rate_limit=$limit_req_status';
# /etc/nginx/sites-available/example.com

server {
    listen 80;
    server_name example.com;

    access_log /var/log/nginx/example.access.log ratelimit;
    error_log /var/log/nginx/example.error.log warn;

    # 全局限制
    limit_req zone=general burst=20 nodelay;
    limit_conn addr 30;

    # 自定义429页面
    error_page 429 /429.html;
    location = /429.html {
        root /var/www/error;
        internal;
        add_header Retry-After 5 always;
    }

    location /login {
        limit_req zone=login burst=3 nodelay;
        limit_conn addr 5;
        proxy_pass http://127.0.0.1:3000;
    }

    location /api/ {
        limit_req zone=api burst=50 delay=30;
        limit_conn addr 20;
        proxy_pass http://127.0.0.1:3000;
    }

    location / {
        proxy_pass http://127.0.0.1:3000;
    }
}
# /etc/fail2ban/jail.local

[nginx-limit-req]
enabled  = true
port     = http,https
filter   = nginx-limit-req
logpath  = /var/log/nginx/example.error.log
maxretry = 10
findtime = 60
bantime  = 600

[nginx-limit-req-repeat]
enabled  = true
port     = http,https
filter   = nginx-limit-req
logpath  = /var/log/nginx/example.error.log
maxretry = 30
findtime = 3600
bantime  = 86400

写完所有配置文件后:

sudo nginx -t && sudo systemctl reload nginx
sudo systemctl restart fail2ban

要了解更全面的安全配置,包括header、TLS和其他加固措施,参见。

指令参考

指令 上下文 默认值 起始版本
limit_req_zone http - 0.7.21
limit_req http, server, location - 0.7.21
limit_req_status http, server, location 503 1.3.15
limit_req_log_level http, server, location error 0.8.18
limit_req_dry_run http, server, location off 1.17.1
limit_conn_zone http - 1.1.8
limit_conn http, server, location - 0.7.21
limit_conn_status http, server, location 503 1.3.15
limit_conn_log_level http, server, location error 0.8.18
limit_conn_dry_run http, server, location off 1.17.6

故障排除

**限流完全不起作用:**检查limit_req_zone是否在http块中,而不是在server块内。zone必须在被引用之前定义。如果你的配置使用include指令,确保zone文件在server块之前被包含。

**合法用户收到429:**降低rate,增加burst,或从无burst切换到burst=N nodelay。使用dry_run模式在设定限制前测量实际流量模式。检查HTTP/2多路复用是否导致limit_conn计数虚高。

**fail2ban不封禁:**确认logpath与Nginx实际写入错误日志的位置匹配。检查limit_req_log_level是否设置为warnerror(默认值)。使用sudo fail2ban-client status nginx-limit-req验证jail是否活跃。如果从localhost测试,注意fail2ban默认忽略服务器自身的IP(ignoreself = true)。从外部机器测试。

限流消息未出现在错误日志中:error_log指令默认为error级别,这会过滤掉limit_req_log_level warn产生的warn级别消息。在server块中设置error_log /var/log/nginx/error.log warn;以查看限流事件。

zone内存耗尽:10m的zone可容纳约160,000个IPv4状态。如果日志中出现could not allocate node,增大zone大小。通过访问日志中的$limit_req_status监控zone使用情况。

**在负载均衡器或CDN后面:**如果Nginx只看到负载均衡器的IP,限流就会应用到那一个IP。使用$http_x_forwarded_for$realip_remote_addr作为zone键替代$binary_remote_addr。同时需要用负载均衡器的IP范围配置set_real_ip_from。只信任来自已知代理的X-Forwarded-For,因为客户端可以伪造它。

日志是你主要的调试工具:

# 实时跟踪限流事件
sudo tail -f /var/log/nginx/error.log | grep "limiting"

# 检查fail2ban操作
sudo journalctl -u fail2ban -f

版权所有 2026 Virtua.Cloud。保留所有权利。 本内容为 Virtua.Cloud 团队原创作品。 未经书面许可,禁止复制、转载或再分发。

准备好亲自尝试了吗?

几秒内部署您自己的服务器。支持 Linux、Windows 或 FreeBSD。

查看 VPS 方案