目录

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_reqhttp2_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;
    }
}

几个关键点:

  1. Access-Control-Allow-Credentials: true 时不能用 Allow-Origin: *,必须回显具体的 origin。这就是要用 map 做白名单的原因。
  2. Vary: Origin 必须加。不加的话 CDN/缓存会把针对 origin A 的响应给 origin B,导致跨域失败或安全问题。
  3. Access-Control-Max-Age 让浏览器缓存预检结果,大幅减少 OPTIONS 请求数量。
  4. if ($request_method = OPTIONS) 这里用 if 是安全的,因为里面只有 add_header + returnreturnif 里是安全用法之一)。

不推荐 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:burstnodelay 分别是什么作用?三种组合有什么区别?

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_reqlimit_conn 的区别?能互相替代吗?

limit_req 限请求速率(漏桶算法),limit_conn 限并发连接数(计数器)。不能替代:HTTP/1.1 keepalive 下一个连接能发无限多请求,所以 limit_conn 挡不住高频请求;反过来 limit_req 挡不住「少量连接长期占用带宽」(比如大文件下载)。HTTP/2 下 limit_conn 效果更弱(一个连接可并发几百个 stream),必须靠 limit_reqhttp2_max_concurrent_streams

Q:怎么让某些 IP 或用户不受限流?

geomap 把这些请求的限流 key 映射成空字符串——limit_req/limit_conn 遇到空 key 会直接跳过。这比写多个 location 干净。

Q:Slowloris 攻击是什么?Nginx 怎么防?

攻击者建立大量连接,每个连接都极慢地发送请求头(每几秒一个字节),让服务器一直等待完整请求,耗尽连接资源。核心防护是 client_header_timeout(设成 10s 左右),配合 client_body_timeoutlimit_connreset_timedout_connection on。通过 stub_statusReading 指标可以发现攻击——正常应该是个位数,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 配置