代码 Nginx 限流、IP 白名单与 CORS 跨域配置

2026-09-03 00:46:08

Nginx 限流、IP 白名单与 CORS 跨域配置

文中 IP 与 example.com 等域名均为示例,实际使用时按自己网络环境替换。

Nginx 作为反向代理入口时,有两类配置经常要一起处理:一类是用限流挡住突发流量;另一类是前后端分离后的跨域响应头。下面按这两块记录配置方式和需要注意的取舍。

两个限流模块

Nginx 自带的限流从两个维度起作用:

  • limit_conn_zone / limit_conn:限制同一个 key(通常是 IP)的并发连接数。适合下载大文件、长连接、防多线程并发下载打满后端。
  • limit_req_zone / limit_req:限制单位时间请求数,比如 rate=10r/s 表示每秒 10 个请求。适合防接口刷票、防 CC、收敛接口调用频率。

两个 zone 可以叠加使用,分别卡住“连接数”和“请求速率”。

IP 白名单 + 限流

办公网、内网服务器等内部来源通常需要放行,只对公网来源做限流。这个需求用 geo + map 两级处理比较干净:

  • geo 根据来源 IP 命中网段,给变量赋值,白名单设为 0,非白名单设为 1
  • map 把这个值转成真正给限流模块用的 key:0 转成空字符串,1 转成 $binary_remote_addr

限流 zone 的 key 用 $limit_key。limit 模块遇到空字符串 key 时不计数,相当于直接放行;非白名单 IP 则按客户端地址计数限流。把“是否白名单”和“用什么做 key”拆成两层后,后续加白名单只动 geo 块,不需要碰限流配置。

http {
# geo:判断来源 IP 是否白名单
geo $limit_whitelist {
default         1;             # 默认全部限流
127.0.0.1       0;             # 本机回环
192.168.0.0/16  0;             # 内网地址段
10.0.0.0/8      0;             # 内网地址段
# x.x.x.x       0;             # 办公网或专线出口,按需追加
}

# map:白名单标记转成限流 key
map $limit_whitelist $limit_key {
0     "";
1     $binary_remote_addr;
}

# 白名单请求 $limit_key 为空字符串,不计数
limit_conn_zone $limit_key zone=conn_zone:10m;
limit_req_zone  $limit_key zone=req_zone:10m rate=10r/s;

server {
# 单 IP 请求频率:基础 10r/s,突发 30,不延迟
limit_req zone=req_zone burst=30 nodelay;

# 单 IP 并发连接数上限
limit_conn conn_zone 15;

limit_req_status 429;

error_page 429 /429.html;
location = /429.html {
default_type text/html;
return 200 "请求频率过高,请稍后再试~
";
}
}
}

几个点值得单独说一下。

rate=10r/s + burst=30 表示速率本身是每秒 10 个,超过速率的请求先进 burst 额度。nodelay 去掉的是排队延迟,也就是 burst 额度内的请求到了就处理,不等待。去掉 nodelay 后,这些请求会排队并按 rate 的节奏放给后端,代价是请求被拉长。是否使用 nodelay,取决于业务更怕延迟还是更怕瞬时打满后端。

limit_conn conn_zone 15 限制的是并发连接数。如果后端有下载类长耗时请求,同一出口 IP 很容易堆连接,这个值需要按实际业务调,不适合原样照抄。

error_page 里的 return 200 比较关键。限流触发时状态码是 429,但进入该 location 后会被 return 200 改写,最终客户端看到的是 200 + 一段 HTML。给浏览器展示“请求频率过高”的友好页面没问题;如果调用方是 API,需要按 429 做重试或降级,就不应该在这里 return 200,应保留 429 状态或直接返回带 429 的响应体。

这里还有一层边界:Nginx 取的是与它直连的客户端地址。如果 Nginx 前面还有 CDN、SLB 等代理,需要在代理层解决真实 IP 透传,否则 geo 白名单和限流 key 都会不准。

CORS 跨域响应头

前后端分域时,浏览器会拦截跨域响应。Nginx 要做的并不是简单加一个固定的 Access-Control-Allow-Origin,而是根据请求里的 Origin 头判断来源是否允许。

这里用 map 动态回显允许的来源:

http {
map $http_origin $cors_origin {
default                          https://mydomain.example.com;
"~*(^https?://([a-z0-9-]+\.)?example\.com$)" $http_origin;
}

server {
location / {
add_header 'Access-Control-Allow-Origin'      $cors_origin always;
add_header 'Access-Control-Allow-Credentials' 'true'       always;
add_header 'Access-Control-Expose-Headers'    'Content-Length,Content-Range' always;

# 后端如果自带 CORS 头,这里先隐藏,避免响应头重复
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Credentials;
proxy_hide_header Access-Control-Expose-Headers;
}
}
}

匹配逻辑:https://www.example.comhttps://api.example.com 这类请求会把原始 Origin 回显到 Access-Control-Allow-Origin;来自 https://other.com 的请求命中 default,返回主域名。这个主域名与请求来源不一致,浏览器仍然会拦截,并不是说 default 会放行所有来源。

default 不能配成空字符串。空值等于不给客户端输出这个头,跨域请求会直接失败;给一个主域名兜底后,至少不匹配来源拿到的响应头是明确的。

由于这里启用了 Access-Control-Allow-Credentials: trueAccess-Control-Allow-Origin 不能用 *,必须返回具体来源。上面的 map 回显方式也基于这个原因。

去掉 always 时,Nginx 只在部分响应码下输出 add_header 指定的头,4xx/5xx 错误响应可能不带 CORS 头。后端一旦报错,浏览器会先把错误响应拦截掉,前端只看到 CORS error,问题很难定位。always 保证所有响应都带上这些头。

proxy_hide_header 解决的是重复头问题。后端应用或网关如果自己返回过 CORS 头,Nginx 转发时再加上一条同名头,客户端收到的就是两个不同值的 Access-Control-Allow-Origin。需要先隐藏后端同名头,再由 Nginx 统一输出。

注意上面的配置只处理了 CORS 响应头,没有覆盖浏览器预检请求(OPTIONS)。如果接口存在自定义 Header、非简单方法等会触发 preflight 的情况,还要单独处理 OPTIONS 请求,并确认后端对预检请求不会执行真正的业务逻辑。

需要复核的配置点

  • geomap 定义在 http 块;limit_reqlimit_connserverlocation 里引用。
  • 白名单值统一为 0,非白名单为 1map0 对应空字符串才会放行。
  • limit_req_zonerate 是限流速率,burstnodelay 决定突发处理方式。
  • 429 错误页若用 return 200 改写,最终响应码不是 429,API 场景慎用。
  • CORS 的 default 不能为空,add_header 记得加 always
  • 后端已返回 CORS 头时,先 proxy_hide_headeradd_header,避免重复。

配置改完先执行 nginx -t,通过后 nginx -s reload 平滑加载。上面写的 10r/sburst=30、并发 15 这些参数是按示例场景给出的,真实业务需要按流量曲线调整。

复制全文 生成海报 Nginx 运维 跨域 限流

推荐文章

程序员茄子在线接单