← 返回笔记列表

Nginx 反向代理的几个容易写错的配置项

这几年配 Nginx 反向代理的次数不算少,但每次新起一个服务还是会在同样几个地方卡住。这次干脆把踩过的坑集中记一遍,下回直接抄。

一、漏配 proxy_set_header,后端拿到的全是 127.0.0.1

最典型的症状:应用日志里所有请求的来源 IP 都是 127.0.0.1 或者内网网关地址,限流、风控、访问统计全部失效。

原因是 Nginx 转发请求时会重新构造 HTTP 请求头,默认只带 HostConnection,客户端的真实信息不会自动传下去。必须显式加上:

location /api/ {
    proxy_pass http://127.0.0.1:8080;

    proxy_set_header Host              $host;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

几个变量的区别值得说清楚:

变量含义
$remote_addr直连 Nginx 的那一跳的 IP
$proxy_add_x_forwarded_for在已有 XFF 后面追加 $remote_addr,形成完整链路
$host请求行或 Host 头里的域名,不带端口
$schemehttphttps

X-Forwarded-Proto 这一条经常被忽略。如果 HTTPS 在 Nginx 层终止,后端应用看到的是明文 HTTP,框架生成的绝对 URL、重定向地址就会退化成 http://,浏览器随即报混合内容错误。

注意:如果 Nginx 直接暴露在公网,客户端伪造的 X-Forwarded-For 会被原样追加。后端解析真实 IP 时应当从右往左数,或者在 Nginx 层用 proxy_set_header X-Forwarded-For $remote_addr; 直接覆盖。

WebSocket 还要额外两行

握手阶段依赖 UpgradeConnection 头,而这两个是逐跳头部,默认不会转发,连接会稳定地停在 101 之前:

proxy_http_version 1.1;
proxy_set_header Upgrade    $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;

proxy_http_version 默认是 1.0,不改的话 Upgrade 机制根本不生效。长连接还要把 proxy_read_timeout 调大,否则默认 60 秒无数据就会被断开。

二、proxy_pass 末尾那个斜杠

这是我个人栽得最多的一处。proxy_pass 的地址带不带结尾斜杠,转发出去的路径完全不同。

假设请求是 /api/user/list

配置实际转发路径
proxy_pass http://backend;/api/user/list
proxy_pass http://backend/;/user/list

规律是:URI 部分为空时,原始路径原封不动转发;带了 URI(哪怕只是一个 /),Nginx 会把 location 匹配到的前缀替换成这个 URI。

所以后端如果自己带 /api 前缀就用不带斜杠的写法,如果后端路由是从根开始的就用带斜杠的写法。写反的表现是清一色 404,但看 Nginx 日志一切正常,很容易往后端方向排查半天。

还有一个坑:只要 proxy_pass 里用了变量,就必须自己拼完整路径,否则默认不传递 URI。

# 变量写法必须显式带上 $request_uri
set $backend "http://127.0.0.1:8080";
proxy_pass $backend$request_uri;

三、location 匹配优先级

Nginx 的 location 不是从上往下先匹配先赢,顺序是固定的:

  1. = 精确匹配,命中立刻结束
  2. ^~ 前缀匹配,命中后不再尝试正则
  3. 正则 ~(区分大小写)和 ~*(不区分),按配置文件里的书写顺序,第一个命中就用
  4. 普通前缀匹配,取最长的那一条

踩过的具体例子:静态资源配了一条正则处理缓存,同时又配了 /static/ 的普通前缀想单独指向另一个目录,结果正则优先级更高,前缀那条永远不生效。

# 想让 /static/ 走这里,必须用 ^~ 阻断后面的正则
location ^~ /static/ {
    alias /var/www/assets/;
    expires 30d;
}

location ~* \.(js|css|png|jpg|gif|ico)$ {
    expires 7d;
    access_log off;
}

alias 和 root 别混用

root 是把 location 路径拼在后面,alias 是把 location 路径整段替换掉:

location /static/ {
    root /var/www;      # 实际路径 /var/www/static/xxx
}

location /static/ {
    alias /var/www/assets/;   # 实际路径 /var/www/assets/xxx
}

alias 时,如果 location 以斜杠结尾,alias 的路径也必须以斜杠结尾,少一个就会拼出 /var/www/assetsxxx 这种诡异路径。

四、改完记得先检查再重载

nginx -t              # 语法检查,会打印出错的行号
nginx -s reload       # 平滑重载,不断开已有连接

直接 restart 会中断正在处理的请求,日常改配置一律用 reload。另外 nginx -T(大写)能把所有 include 进来的配置合并打印出来,排查「这条配置到底有没有被加载」时非常好用。


小结一下这次的教训:反向代理出问题时,先用 curl -v 直连后端端口确认服务本身正常,再对比 Nginx 转发过去的请求头差在哪。大部分「后端好好的,一过 Nginx 就不对」的问题,答案都在被丢掉的那几个头里。