Nginx限流与DDoS防护
通过配置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_zone和limit_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_req。general 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_conn和limit_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工作流程:
- 在配置中添加
limit_req_dry_run on; - 重载Nginx
- 生成测试流量(参见下面的测试部分)
- 检查错误日志中的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;已处理此问题。
- 检查访问日志中的
$limit_req_status变量:
sudo grep "REJECTED_DRY_RUN\|DELAYED_DRY_RUN" /var/log/nginx/access.log
$limit_req_status变量返回以下值之一:PASSED、DELAYED、REJECTED、DELAYED_DRY_RUN或REJECTED_DRY_RUN。
- 当速率看起来合适时,删除
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/s、burst=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是否设置为warn或error(默认值)。使用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 团队原创作品。 未经书面许可,禁止复制、转载或再分发。