Nginx-09 限流限连与安全防护
前置阅读:Nginx-08 缓存机制
Nginx 在最前面,是最合适做流量管控的位置。这一篇讲清限流的原理(尤其是 burst/nodelay 那三个容易搞混的模式)和常见攻击的防护。
1. limit_req —— 请求速率限流
1.1 基础配置
http {
# 定义限流区(必须在 http 层)
limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
server {
location /api/ {
limit_req zone=perip;
proxy_pass http://backend;
}
}
}
三个参数:
- key:按什么维度限流。
$binary_remote_addr是二进制形式的 IP,必须用这个而不是$remote_addr——前者 IPv4 占 4 字节,后者是字符串占 7-15 字节。 - zone=名字:大小:共享内存区。1MB 约能存 16000 个 IPv4 状态(每个状态 64 字节)。
- rate:速率,支持
r/s(每秒)和r/m(每分钟)。rate=10r/s内部换算成每 100ms 允许 1 个请求。
1.2 漏桶算法(leaky bucket)
Nginx 的 limit_req 用的是漏桶算法,核心特征是「匀速」。
请求流入(可能是突发的)
↓↓↓↓↓
┌─────────┐
│ 桶 │ ← burst 决定桶的容量
│ ○ ○ ○ │
└────┬────┘
│ 匀速漏出,速率 = rate
↓
后端服务
关键理解:rate=10r/s 不是「每秒最多 10 个」,而是「每 100ms 最多 1 个」。
这个区别非常重要。如果同一毫秒内来了 2 个请求,即使这一秒总共只有 2 个请求,第二个也会被拒绝(503),因为它没有等到 100ms 的间隔。
很多人配了限流后发现「明明 QPS 没超,怎么大量 503」,原因就在这里。
1.3 burst —— 允许突发
limit_req zone=perip burst=20;
burst=20 表示桶的容量是 20:超出速率的请求可以先排队,最多排 20 个,超过 20 个才拒绝。
排队的请求会被延迟处理,以满足 rate 的匀速要求。
rate=10r/s(每 100ms 1 个),burst=20
瞬间来了 25 个请求:
第 1 个 → 立即处理
第 2-21 个 → 进队列,分别延迟 100ms, 200ms, ..., 2000ms 后处理
第 22-25 个 → 队列满,直接返回 503
问题:第 21 个请求要等 2 秒才被处理。对用户来说这就是「卡了 2 秒」,体验很差,而且这些等待的连接一直占着资源。
1.4 nodelay —— 不延迟
limit_req zone=perip burst=20 nodelay;
nodelay 的含义是:队列里的请求立即处理,但队列槽位仍然按 rate 的速度释放。
rate=10r/s,burst=20,nodelay
瞬间来了 25 个请求:
第 1-21 个 → 全部立即处理(占满 1 + 20 个槽位)
第 22-25 个 → 503
之后槽位按每 100ms 释放 1 个:
100ms 后 → 可以再接受 1 个
1 秒后 → 释放了 10 个槽位
2 秒后 → 槽位全部释放,又能接受 21 个突发
这才是生产环境应该用的配置。它的效果是「允许合理的突发,长期速率受控」,符合真实业务的流量特征——用户打开一个页面会瞬间发 10 个 API 请求,这是正常行为,不该被限流。
1.5 delay —— 混合模式(1.15.7+)
limit_req zone=perip burst=20 delay=8;
前 8 个超额请求立即处理(像 nodelay),第 9 到 20 个进队列延迟处理(像纯 burst),超过 20 个拒绝。
适合「小突发放行、大突发限速、超大突发拒绝」的三段式策略。
1.6 三种模式对照实验
http {
limit_req_zone $binary_remote_addr zone=test:10m rate=2r/s;
server {
listen 8080;
default_type text/plain;
location /a { limit_req zone=test; return 200 "a\n"; }
location /b { limit_req zone=test burst=5; return 200 "b\n"; }
location /c { limit_req zone=test burst=5 nodelay; return 200 "c\n"; }
location /d { limit_req zone=test burst=5 delay=2; return 200 "d\n"; }
}
}
# 瞬间发 8 个请求,观察状态码和耗时
for p in a b c d; do
echo "=== /$p ==="
for i in $(seq 8); do
curl -s -o /dev/null -w "%{http_code} %{time_total}s\n" \
"http://127.0.0.1:8080/$p" &
done
wait
sleep 5 # 等桶清空
done
预期结果:
| location | 配置 | 结果 |
|---|---|---|
/a |
无 burst | 1 个 200,7 个 503 |
/b |
burst=5 |
6 个 200(但耗时递增到 2.5s),2 个 503 |
/c |
burst=5 nodelay |
6 个 200(全部立即返回),2 个 503 |
/d |
burst=5 delay=2 |
3 个立即 200,3 个延迟 200,2 个 503 |
1.7 多维度限流
多个 limit_req 可以叠加,所有条件都要满足(任一触发就拒绝):
http {
# 单 IP 限流
limit_req_zone $binary_remote_addr zone=perip:20m rate=20r/s;
# 全站总限流(保护后端不被打爆)
limit_req_zone $server_name zone=perserver:10m rate=2000r/s;
# 按登录用户限流(比 IP 精确,能防同 IP 多账号)
limit_req_zone $cookie_uid zone=peruser:20m rate=30r/s;
# 按 API Key 限流(开放平台场景)
limit_req_zone $http_x_api_key zone=perkey:20m rate=100r/s;
server {
location /api/ {
limit_req zone=perip burst=40 nodelay;
limit_req zone=perserver burst=4000 nodelay;
limit_req zone=peruser burst=60 nodelay;
proxy_pass http://backend;
}
# 敏感接口单独收紧
location = /api/login {
limit_req zone=perip burst=3 nodelay; # 登录接口严格限
proxy_pass http://backend;
}
location = /api/sms/send {
limit_req zone=perip burst=1 nodelay; # 短信接口最严格
proxy_pass http://backend;
}
}
}
1.8 差异化限流(白名单 / VIP 不限)
用 map 把不需要限流的 key 映射成空字符串——key 为空时 limit_req 直接跳过:
# 内网 IP 和白名单不限流
geo $limit_by_ip {
default 1;
10.0.0.0/8 0;
172.16.0.0/12 0;
127.0.0.1/32 0;
1.2.3.4/32 0; # 合作方 IP
}
map $limit_by_ip $limit_key {
1 $binary_remote_addr;
0 ""; # 空 key → 不限流
}
limit_req_zone $limit_key zone=perip:20m rate=20r/s;
按用户等级差异化:
map $cookie_level $rate_key_vip {
default $binary_remote_addr; # 普通用户按 IP 限
"vip" ""; # VIP 不限
}
1.9 限流的响应与日志
http {
# 被限流时返回的状态码,默认 503
# 429 Too Many Requests 语义更准确,客户端也更容易识别
limit_req_status 429;
# 限流日志的级别,默认 error
# 设成 warn 可以让 error.log 不那么吵
limit_req_log_level warn;
server {
# 给限流响应加上 Retry-After,告诉客户端多久后再试
error_page 429 = @too_many;
location @too_many {
default_type application/json;
add_header Retry-After 1 always;
return 429 '{"code":429,"message":"too many requests"}';
}
}
}
限流日志的样子:
2026/08/03 10:30:15 [warn] 1234#0: *5678 limiting requests, excess: 20.500 by zone "perip",
client: 1.2.3.4, server: example.com, request: "GET /api/list HTTP/1.1", host: "example.com"
excess 表示超出的请求数(乘以 1000 后的值,这里 20.5 表示超了 20.5 个)。
统计被限流最多的 IP:
grep "limiting requests" /var/log/nginx/error.log \
| grep -oP 'client: \K[\d.]+' \
| sort | uniq -c | sort -rn | head -20
2. limit_conn —— 并发连接限制
http {
limit_conn_zone $binary_remote_addr zone=perip_conn:10m;
limit_conn_zone $server_name zone=perserver_conn:10m;
limit_conn_status 429;
limit_conn_log_level warn;
server {
# 单 IP 最多 20 个并发连接
limit_conn perip_conn 20;
# 全站最多 5000 个并发连接
limit_conn perserver_conn 5000;
# 下载接口收紧
location /download/ {
limit_conn perip_conn 2;
limit_rate 5m;
}
}
}
2.1 limit_req 和 limit_conn 的区别
limit_req |
limit_conn |
|
|---|---|---|
| 限制对象 | 请求速率(每秒多少个请求) | 并发连接数(同时有多少条连接) |
| 算法 | 漏桶 | 计数器 |
| 防什么 | CC 攻击、接口滥刷、爬虫 | 下载占用带宽、连接耗尽、慢速攻击 |
| 计数时机 | 每个请求到来时 | 连接建立到关闭的整个期间 |
注意 HTTP/1.1 长连接和 HTTP/2 的影响:
- HTTP/1.1 keepalive 下,一个连接可以发很多请求。
limit_conn 20允许 20 个连接,但每个连接可以持续发请求——所以limit_conn不能替代limit_req。 - HTTP/2 一个连接可以并发很多 stream。
limit_conn在 HTTP/2 下的限制效果很弱(一个连接就够客户端发几百个并发请求了)。HTTP/2 场景下必须靠limit_req和http2_max_concurrent_streams。
http {
http2_max_concurrent_streams 128; # 限制单个 HTTP/2 连接的并发 stream 数
}
3. limit_rate —— 带宽限速
location /download/ {
limit_rate 500k; # 单连接 500KB/s
limit_rate_after 10m; # 前 10MB 不限速(提升"秒开"体验)
}
limit_rate 是单连接的。客户端开 10 个并发就能占 5MB/s,所以必须配合 limit_conn:
limit_conn_zone $binary_remote_addr zone=dl:10m;
location /download/ {
limit_conn dl 2;
limit_rate 500k;
limit_rate_after 10m;
}
动态限速(用变量):
map $cookie_vip $user_rate {
default "200k";
"1" "2m";
"2" "0"; # 0 表示不限速
}
location /download/ {
limit_rate $user_rate;
}
set $limit_rate 也可以在 Lua 或 if 里动态设置:
location /download/ {
set $limit_rate 500k; # 也能用这种方式设置
}
4. CC 攻击防护
CC(Challenge Collapsar)攻击就是用大量看起来正常的 HTTP 请求打垮服务。
4.1 分层防护策略
http {
# ---------- 第一层:连接数 ----------
limit_conn_zone $binary_remote_addr zone=cc_conn:20m;
# ---------- 第二层:请求速率(多档) ----------
limit_req_zone $binary_remote_addr zone=cc_general:20m rate=30r/s;
limit_req_zone $binary_remote_addr zone=cc_strict:20m rate=2r/s;
limit_req_zone $server_name zone=cc_global:10m rate=5000r/s;
# ---------- 第三层:UA 与 Referer 特征 ----------
map $http_user_agent $bad_ua {
default 0;
"" 1; # 空 UA
"~*(?:sqlmap|nikto|nmap|masscan|dirbuster|acunetix|nessus)" 1;
"~*(?:python-requests|go-http-client|curl|wget|scrapy)" 1;
# 注意:正经的爬虫(Googlebot/Baiduspider)不要拦,用 robots.txt 管
}
map $http_user_agent $good_bot {
default 0;
"~*(?:Googlebot|Bingbot|Baiduspider|YandexBot|Sogou)" 1;
}
server {
listen 443 ssl;
http2 on;
# 拦掉恶意 UA(但放过正经搜索引擎)
if ($bad_ua) {
return 403;
}
limit_conn cc_conn 30;
limit_req zone=cc_general burst=60 nodelay;
limit_req zone=cc_global burst=10000 nodelay;
# 敏感接口严格限流
location = /api/login { limit_req zone=cc_strict burst=3 nodelay; proxy_pass http://backend; }
location = /api/register { limit_req zone=cc_strict burst=2 nodelay; proxy_pass http://backend; }
location = /api/sms/send { limit_req zone=cc_strict burst=1 nodelay; proxy_pass http://backend; }
location = /api/password/reset { limit_req zone=cc_strict burst=2 nodelay; proxy_pass http://backend; }
location = /api/captcha { limit_req zone=cc_strict burst=5 nodelay; proxy_pass http://backend; }
location /api/ {
proxy_pass http://backend;
}
}
}
4.2 慢速攻击(Slowloris)防护
Slowloris 的原理:建立大量连接,但每个连接都极慢地发送请求头(比如每 10 秒发一个字节),让服务器一直等着,耗尽连接数。
http {
# ① 读请求头的超时——这是防 Slowloris 的关键
client_header_timeout 10s;
# ② 读请求体的超时——防 Slow POST(R.U.D.Y 攻击)
client_body_timeout 10s;
# ③ 请求头大小限制
client_header_buffer_size 4k;
large_client_header_buffers 4 8k;
# ④ 请求体大小限制
client_max_body_size 10m;
# ⑤ 发送响应的超时
send_timeout 10s;
# ⑥ 超时连接直接 RST,立即回收资源(而不是走正常的 FIN 四次挥手)
reset_timedout_connection on;
# ⑦ 限制单 IP 连接数(Slowloris 需要大量连接)
limit_conn_zone $binary_remote_addr zone=slow:10m;
server {
limit_conn slow 20;
# ⑧ 限制 keepalive
keepalive_timeout 30s;
keepalive_requests 500;
}
}
client_header_timeout 是最有效的一条。Slowloris 依赖「服务器无限期等待完整请求头」,把这个超时设到 10 秒就基本失效了。
配合 stub_status 能观察到攻击:
curl http://127.0.0.1/nginx_status
# Reading: 850 Writing: 3 Waiting: 12
# ↑ Reading 异常高 = 大量连接停在"读请求头"阶段 = Slowloris 特征
正常情况下 Reading 应该是个很小的数字(个位数到几十)。
4.3 内核层配合
# /etc/sysctl.conf
# SYN Flood 防护
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 65536
net.ipv4.tcp_synack_retries = 2
# 加大 accept 队列(配合 nginx 的 listen backlog)
net.core.somaxconn = 65535
# TIME_WAIT 快速回收
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
# 扩大端口范围(做反向代理时需要大量本地端口)
net.ipv4.ip_local_port_range = 1024 65000
sysctl -p
# Nginx 侧对应
server {
listen 443 ssl backlog=65535 reuseport;
}
注意
net.ipv4.tcp_tw_recycle在 Linux 4.12 已被移除,NAT 环境下会导致连接失败,不要再配它。详见 网络-10 TCP 实战调优。
5. 访问控制
5.1 IP 黑白名单
# 简单方式
location /admin/ {
allow 192.168.1.0/24;
allow 10.0.0.0/8;
deny all;
}
# 大量 IP 用 geo 模块(内部用基数树,查找是 O(log n),比一堆 allow/deny 快得多)
geo $blocked_ip {
default 0;
include /etc/nginx/conf.d/blacklist.conf; # 每行 "1.2.3.4/32 1;"
}
server {
if ($blocked_ip) {
return 403;
}
}
blacklist.conf 内容:
1.2.3.4/32 1;
5.6.7.0/24 1;
更新黑名单只需 nginx -s reload(几万条 IP 也很快)。想做到不 reload 就更新,需要 OpenResty + 共享内存或 Redis。
5.2 geo 模块做地域限制
# 需要 GeoIP2 模块和 MaxMind 数据库
# --with-module=ngx_http_geoip2_module
http {
geoip2 /usr/share/GeoIP/GeoLite2-Country.mmdb {
auto_reload 1d;
$geoip2_country_code country iso_code;
}
map $geoip2_country_code $allowed_country {
default 0;
CN 1;
HK 1;
TW 1;
SG 1;
}
server {
if ($allowed_country = 0) {
return 403;
}
}
}
5.3 HTTP Basic 认证
# 生成密码文件
htpasswd -c /etc/nginx/.htpasswd admin
# 或者不装 apache2-utils:
printf "admin:$(openssl passwd -apr1 'yourpassword')\n" > /etc/nginx/.htpasswd
location /admin/ {
auth_basic "Restricted Area";
auth_basic_user_file /etc/nginx/.htpasswd;
# 内网免密(satisfy any)
satisfy any;
allow 10.0.0.0/8;
deny all;
}
Basic 认证的密码是 base64 编码明文传输的,必须配合 HTTPS 使用。
5.4 limit_except —— 限制 HTTP 方法
location /api/data {
# 除了 GET 和 HEAD,其他方法都要满足下面的条件
limit_except GET HEAD {
allow 10.0.0.0/8;
deny all;
}
proxy_pass http://backend;
}
# 只读接口:完全禁止写方法
location /api/public/ {
limit_except GET HEAD OPTIONS {
deny all;
}
proxy_pass http://backend;
}
注意 limit_except GET 自动包含 HEAD(HEAD 是 GET 的子集)。
这比 if ($request_method = POST) 好,因为它在 ACCESS 阶段执行,不会有 if 的上下文问题。
6. 常见漏洞防护
6.1 屏蔽敏感路径
server {
# 隐藏文件和目录(.git 泄露是最常见的信息泄露)
location ~ /\. {
deny all;
access_log off;
log_not_found off;
return 404;
}
# 配置和备份文件
location ~* \.(bak|backup|old|orig|save|swp|swo|tmp|sql|log|conf|config|ini|env|yml|yaml|lock)$ {
return 404;
}
# 版本控制目录
location ~ /(\.git|\.svn|\.hg|\.bzr|CVS)/ {
return 404;
}
# 常见的敏感文件
location ~* /(composer\.(json|lock)|package(-lock)?\.json|Gemfile(\.lock)?|
\.htaccess|\.htpasswd|web\.config|wp-config\.php|
docker-compose\.ya?ml|Dockerfile|Makefile)$ {
return 404;
}
# 探测性扫描路径(直接 444 断连,让扫描器超时)
location ~* /(phpmyadmin|pma|myadmin|adminer|wp-admin|wp-login|
\.env|actuator|swagger-ui|api-docs|druid|solr)/ {
return 444;
}
# sourcemap
location ~* \.map$ {
return 404;
}
}
用 404 而不是 403:403 告诉攻击者「这里有东西但你没权限」,404 让他以为不存在,信息泄露更少。对明确的扫描器用 444(直接断连),让它的每次探测都要等超时,大幅降低扫描速度。
6.2 上传目录禁止执行
这是防 WebShell 的关键:
location ^~ /uploads/ {
root /data;
# 白名单:只允许这些类型
location ~* \.(jpe?g|png|gif|webp|avif|pdf|docx?|xlsx?|zip)$ {
expires 30d;
add_header Content-Disposition "attachment" always; # 强制下载,不在浏览器打开
}
# 黑名单:明确拒绝所有可执行类型
location ~* \.(php[0-9]?|phtml|pht|jsp[x]?|asp[x]?|cer|cdx|htaccess|
sh|bash|py|pl|rb|cgi|exe|dll|so)$ {
deny all;
return 403;
}
# 其他一律拒绝
location ~ / {
return 403;
}
}
另外要加 X-Content-Type-Options: nosniff,防止浏览器把 .txt 当 HTML 执行。
6.3 安全响应头
# snippets/security-headers.conf
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always; # 老浏览器用,现代浏览器已弃用
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://api.example.com; frame-ancestors 'self'; base-uri 'self'; form-action 'self'" always;
# 隐藏版本号
server_tokens off;
各个头的作用:
| 头 | 防什么 |
|---|---|
X-Content-Type-Options: nosniff |
MIME 类型混淆导致的 XSS |
X-Frame-Options: SAMEORIGIN |
点击劫持(clickjacking) |
Referrer-Policy |
Referer 泄露内部 URL 和参数 |
Strict-Transport-Security |
SSL 剥离攻击(强制浏览器用 HTTPS) |
Content-Security-Policy |
XSS(最有效的一个,但配置最麻烦) |
Permissions-Policy |
限制页面能用的浏览器 API |
CSP 的配置技巧:先用 Content-Security-Policy-Report-Only 上线,收集违规报告但不阻断,确认没有误伤再切成强制模式。
记住 always 参数:不加的话 add_header 只在 2xx/3xx 响应上生效,错误页面不带这些头。
6.4 完全隐藏 Nginx 特征
server_tokens off 只隐藏版本号,响应头里还是 Server: nginx。要完全去掉需要改源码或用第三方模块:
# 方案一:headers-more 模块
# --add-module=../headers-more-nginx-module
more_set_headers "Server: unknown";
more_clear_headers "X-Powered-By"; # 清掉后端泄露的框架信息
# 方案二:改源码(src/http/ngx_http_header_filter_module.c)后重新编译
顺便把后端泄露的头也清掉:
proxy_hide_header X-Powered-By;
proxy_hide_header X-AspNet-Version;
proxy_hide_header X-Runtime;
proxy_hide_header Server;
7. CORS 配置
跨域是前后端分离的必修课,Nginx 层配置的关键是正确处理预检请求。
# 白名单式的 origin 校验(比返回 * 安全)
map $http_origin $cors_origin {
default "";
"~^https://(www\.)?example\.com$" $http_origin;
"~^https://.*\.example\.com$" $http_origin;
"~^http://localhost(:[0-9]+)?$" $http_origin; # 开发环境
}
server {
location /api/ {
# 预检请求(OPTIONS)单独处理,直接返回,不转发给后端
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-Requested-With, X-Request-ID" always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Access-Control-Max-Age 86400 always; # 预检结果缓存 24h
add_header Content-Length 0;
add_header Content-Type "text/plain";
return 204;
}
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Access-Control-Expose-Headers "X-Request-ID, X-Total-Count" always;
add_header Vary Origin always; # 必须!否则 CDN 会缓存错误的 Origin
proxy_pass http://backend;
}
}
几个关键点:
Access-Control-Allow-Credentials: true时不能用Allow-Origin: *,必须回显具体的 origin。这就是要用map做白名单的原因。Vary: Origin必须加。不加的话 CDN/缓存会把针对 origin A 的响应给 origin B,导致跨域失败或安全问题。Access-Control-Max-Age让浏览器缓存预检结果,大幅减少 OPTIONS 请求数量。if ($request_method = OPTIONS)这里用if是安全的,因为里面只有add_header+return(return在if里是安全用法之一)。
不推荐 Access-Control-Allow-Origin: *:一旦接口需要带 Cookie 就得改,而且任何网站都能读你的接口响应。
8. WAF
8.1 ModSecurity
# 编译 ModSecurity + Nginx connector
git clone --depth 1 -b v3/master https://github.com/SpiderLabs/ModSecurity
git clone https://github.com/SpiderLabs/ModSecurity-nginx
./configure --add-dynamic-module=../ModSecurity-nginx
load_module modules/ngx_http_modsecurity_module.so;
http {
modsecurity on;
modsecurity_rules_file /etc/nginx/modsec/main.conf;
}
# /etc/nginx/modsec/main.conf
Include /etc/nginx/modsec/modsecurity.conf
Include /etc/nginx/modsec/crs/crs-setup.conf
Include /etc/nginx/modsec/crs/rules/*.conf
# 先用 DetectionOnly 观察,确认误报可控再切 On
SecRuleEngine DetectionOnly
# SecRuleEngine On
配合 OWASP Core Rule Set (CRS) 能防 SQL 注入、XSS、命令注入、路径穿越等常见攻击。
上线注意:CRS 的误报率不低,必须先跑 DetectionOnly 模式收集日志,逐条排除误报规则,否则一上线就会拦掉正常业务。
8.2 简易的自建规则
不想上 ModSecurity 的话,用正则拦一些明显的攻击特征也有效果:
server {
# SQL 注入特征
if ($query_string ~* "(union.*select|select.*from|insert\s+into|drop\s+table|
benchmark\s*\(|sleep\s*\(|load_file\s*\()") {
return 403;
}
# XSS 特征
if ($query_string ~* "(<script|javascript:|onerror\s*=|onload\s*=|
document\.cookie|eval\s*\()") {
return 403;
}
# 路径穿越(注意:$uri 已被规范化,这里用 $request_uri 查原始值)
if ($request_uri ~* "(\.\./|\.\.%2f|%2e%2e%2f|/etc/passwd|/proc/self)") {
return 403;
}
# 命令注入
if ($query_string ~* "(;|\||`|\$\(|&&).*(cat|ls|wget|curl|bash|sh|nc)\s") {
return 403;
}
}
这种自建规则的局限性要清楚:
- 很容易被编码绕过(URL 编码、双重编码、大小写、注释符)
- 误报率高(正常内容里可能包含这些关键词,比如一篇讲 SQL 的文章)
if在每个请求上跑多个正则,有性能开销
它只是纵深防御的一层,不能替代业务代码里的参数化查询和输出转义。 真正的防线在应用层。
9. 完整的安全配置模板
http {
server_tokens off;
# ---------- 超时(防慢速攻击)----------
client_header_timeout 10s;
client_body_timeout 10s;
send_timeout 10s;
keepalive_timeout 30s;
keepalive_requests 500;
reset_timedout_connection on;
# ---------- 大小限制 ----------
client_max_body_size 10m;
client_body_buffer_size 128k;
client_header_buffer_size 4k;
large_client_header_buffers 4 8k;
# ---------- 限流区 ----------
limit_req_zone $binary_remote_addr zone=req_ip:20m rate=30r/s;
limit_req_zone $binary_remote_addr zone=req_strict:20m rate=2r/s;
limit_req_zone $server_name zone=req_all:10m rate=5000r/s;
limit_conn_zone $binary_remote_addr zone=conn_ip:20m;
limit_req_status 429;
limit_conn_status 429;
limit_req_log_level warn;
# ---------- UA 过滤 ----------
map $http_user_agent $bad_ua {
default 0;
"" 1;
"~*(sqlmap|nikto|nmap|masscan|dirbuster|acunetix|nessus|zgrab)" 1;
}
# ---------- CORS ----------
map $http_origin $cors_origin {
default "";
"~^https://(www\.)?example\.com$" $http_origin;
"~^https://.*\.example\.com$" $http_origin;
}
server {
listen 443 ssl backlog=65535 reuseport;
http2 on;
server_name example.com;
include snippets/ssl-common.conf;
include snippets/security-headers.conf;
if ($bad_ua) { return 403; }
limit_conn conn_ip 30;
limit_req zone=req_ip burst=60 nodelay;
limit_req zone=req_all burst=10000 nodelay;
# 敏感接口
location = /api/login { limit_req zone=req_strict burst=3 nodelay; include snippets/proxy-headers.conf; proxy_pass http://backend; }
location = /api/register { limit_req zone=req_strict burst=2 nodelay; include snippets/proxy-headers.conf; proxy_pass http://backend; }
location = /api/sms/send { limit_req zone=req_strict burst=1 nodelay; include snippets/proxy-headers.conf; proxy_pass http://backend; }
# 屏蔽敏感路径
location ~ /\. { return 404; access_log off; }
location ~* \.(bak|sql|env|conf|ini|log|map)$ { return 404; }
location ~* /(phpmyadmin|wp-admin|actuator|druid)/ { return 444; }
# 上传目录
location ^~ /uploads/ {
root /data;
location ~* \.(jpe?g|png|gif|webp|pdf)$ { expires 30d; }
location ~ / { return 403; }
}
location /api/ {
include snippets/proxy-headers.conf;
proxy_pass http://backend;
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Vary Origin always;
}
location / {
root /var/www/dist;
try_files $uri $uri/ /index.html;
}
}
}
10. 面试题
Q:Nginx 限流用的什么算法?rate=10r/s 具体是什么含义?
漏桶算法。rate=10r/s 内部换算成「每 100ms 允许通过 1 个请求」,而不是「每秒最多 10 个」。所以即使一秒内总共只有 2 个请求,如果它们在同一毫秒到达,第二个也会被拒绝——因为没等到 100ms 的间隔。这是很多人「QPS 明明没超却大量 503」的原因。
Q:burst 和 nodelay 分别是什么作用?三种组合有什么区别?
burst=N 定义桶容量,允许 N 个超额请求排队,排队的请求被延迟处理以满足匀速要求。nodelay 让队列里的请求立即处理,但槽位仍按 rate 的速度释放。
- 只有
burst:请求不被拒绝但要等(第 N 个可能等好几秒),用户体验差。 burst+nodelay:允许瞬时突发全部立即通过,长期速率仍受控。这是生产推荐配置。burst+delay=M:前 M 个立即处理,M 到 burst 之间延迟处理,超出拒绝。三段式策略。
Q:为什么限流的 key 要用 $binary_remote_addr 而不是 $remote_addr?
$binary_remote_addr 是 IP 的二进制表示,IPv4 固定 4 字节;$remote_addr 是点分十进制字符串,7-15 字节。共享内存里存几十万个 key 时,前者能省一半以上内存。1MB 的 zone 用二进制形式约能存 16000 个 IPv4 状态。
Q:limit_req 和 limit_conn 的区别?能互相替代吗?
limit_req 限请求速率(漏桶算法),limit_conn 限并发连接数(计数器)。不能替代:HTTP/1.1 keepalive 下一个连接能发无限多请求,所以 limit_conn 挡不住高频请求;反过来 limit_req 挡不住「少量连接长期占用带宽」(比如大文件下载)。HTTP/2 下 limit_conn 效果更弱(一个连接可并发几百个 stream),必须靠 limit_req 和 http2_max_concurrent_streams。
Q:怎么让某些 IP 或用户不受限流?
用 geo 或 map 把这些请求的限流 key 映射成空字符串——limit_req/limit_conn 遇到空 key 会直接跳过。这比写多个 location 干净。
Q:Slowloris 攻击是什么?Nginx 怎么防?
攻击者建立大量连接,每个连接都极慢地发送请求头(每几秒一个字节),让服务器一直等待完整请求,耗尽连接资源。核心防护是 client_header_timeout(设成 10s 左右),配合 client_body_timeout、limit_conn、reset_timedout_connection on。通过 stub_status 的 Reading 指标可以发现攻击——正常应该是个位数,Slowloris 下会异常高。
Q:add_header 为什么要加 always?
不加 always 时,add_header 只在响应码为 200/201/204/206/301/302/303/304/307/308 时生效。4xx/5xx 错误响应不会带上这些头——安全头和 CORS 头在错误响应上缺失会导致问题(比如浏览器读不到 502 的响应内容,因为缺 CORS 头)。
Q:CORS 配置为什么必须加 Vary: Origin?
Access-Control-Allow-Origin 的值随请求的 Origin 变化。如果不声明 Vary: Origin,CDN 或代理缓存会把针对 origin A 生成的响应(带 Allow-Origin: A)返回给 origin B 的请求,导致 B 的跨域请求失败——或者反过来造成安全问题。
Q:Access-Control-Allow-Origin: * 有什么问题?
(1) 与 Access-Control-Allow-Credentials: true 不兼容,浏览器会直接拒绝,所以带 Cookie 的接口不能用 *;(2) 任何网站都能用 JS 读取你的接口响应。正确做法是用 map 做 origin 白名单,回显具体的 origin 值。
Q:屏蔽敏感路径时返回 404 还是 403?
返回 404。403 等于告诉攻击者「这个路径存在但你没权限」,是信息泄露。404 让他认为路径不存在。对明确的扫描器可以用 Nginx 私有的 444——直接断开 TCP 连接、不返回任何数据,让扫描器每次探测都要等到超时。
上一篇:Nginx-08 缓存机制 | 下一篇:Nginx-10 HTTPS 与 TLS 配置
xingliuhua