代理配置那六行,以及没有这六行会发生什么
一句话总结
代理配置事故往往来自六行请求头转发配置,或 proxy_pass 中一个斜杠。两者即使遗漏,页面平时也能正常显示,因此通常很晚才被发现。
为什么在前面放 Web 服务器
Tomcat 也会处理 HTTP,为什么还要在前面放 nginx?在 SI 现场,真正理由不是性能,而是运维便利与统一策略。
- 不让 WAS 处理静态文件,节省 WAS 线程
- 在一个位置终止 SSL,只需在一台服务器更换证书
- 在多个 WAS 间负载均衡,一台宕机时服务仍可持续
- 按 URL 路由到不同系统(
/api/走新系统,其余走遗留系统) - 集中设置访问控制、请求大小限制、压缩与缓存请求头
最后一点尤其重要。策略放在应用中,修改后必须重新部署;放在代理中,只需 reload。 这就是运维团队偏爱代理的原因。
代理配置的六行
location / {
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;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_connect_timeout 3s;
proxy_send_timeout 30s;
proxy_read_timeout 60s;
}
只有理解每行缺失时会发生什么,才不是死记硬背。
- 缺少
Host $host时,后端收到的 Host 是127.0.0.1:8080。应用生成绝对 URL(重定向、邮件链接、支付回调)时,会发出http://127.0.0.1:8080/...。上线后才发现,支付回调将无法返回。 - 缺少
X-Forwarded-For时,应用看到的所有客户端 IP 都是代理 IP。访问记录、欺诈检测、基于 IP 的访问控制全部失去意义,审计时也无法回答“是否记录访问者 IP”。$proxy_add_x_forwarded_for会追加到已有请求头,因此经过多层代理后会形成列表。 - 缺少
X-Forwarded-Proto是重定向无限循环的首要原因。代理终止 TLS,再以 HTTP 传给后端;后端认为“这不是 HTTPS”,于是重定向到https://。请求再次到达代理,又以 HTTP 转发,如此无限重复,浏览器显示ERR_TOO_MANY_REDIRECTS。绝大多数循环都源于TLS 终止点与应用的 HTTPS 强制逻辑彼此不知情。
还有一条重要安全原则:这些请求头必须在最前端代理被覆盖。 因为客户端可以自行发送 X-Forwarded-For: 10.0.0.1。使用 proxy_set_header 覆盖后,客户端值会被忽略,但追加型变量除外。
proxy_pass 的斜杠——最常见的 nginx 缺陷
location /api/ {
proxy_pass http://backend; # 슬래시 없음 → /api/users/1 이 그대로 전달
}
location /api/ {
proxy_pass http://backend/; # 슬래시 있음 → /api 부분이 잘리고 /users/1 로 전달
}
只要 URI 部分存在(包括斜杠),与 location 匹配的部分就会被删除。 后端期望 /api context,却加了斜杠,就会大量返回 404;反过来,后端期望根路径却未加斜杠,可能得到 /api/api/users。
每年都有人为这一个字符浪费半天。混淆时,同时查看 access log 中的 $upstream_addr 与后端日志中的请求路径,即可立即判断。
请求大小——413 的真相
nginx 的 client_max_body_size 默认值为 1MB。文件上传系统不调高该值,上传 3MB 附件时会出现 413 Request Entity Too Large。而这个错误由 nginx 自己返回,应用日志不会留下任何记录。 开发人员会因“服务器里完全没有日志”而排查数日。
client_max_body_size 20m;
client_body_buffer_size 128k;
large_client_header_buffers 4 16k;
large_client_header_buffers 用于较大的请求头。接入 SSO 后 cookie 会增大,超过默认缓冲区就会返回 400 Bad Request,而日志同样不够明确。
压缩与静态文件
gzip on;
gzip_comp_level 6;
gzip_min_length 1000;
gzip_types text/plain text/css application/json application/javascript text/xml;
gzip_vary on;
gzip_comp_level在 6 左右最能平衡 CPU 与压缩率。提高到 9,体积只减少几个百分点,CPU 开销却明显增加。gzip_min_length 1000——小于 1KB 的内容压缩收益很小,只有额外开销。- 缺少
gzip_vary on时,中间缓存可能把压缩内容发给不支持压缩的客户端。 gzip_types始终包含text/html,无需另写。
静态文件最好完全不经过代理。
location /static/ {
alias /app/static/;
expires 7d;
access_log off;
}
混淆 root 与 alias 也是常见问题。对于 location /static/,root /app; 对应 /app/static/파일,alias /app/static/; 也对应 /app/static/파일。看起来相同,但若 location /s/ 搭配 alias /app/static/;,则 /s/a.js →/app/static/a.js。location 路径与真实目录名不同时使用 alias。
阻止访问管理页面
Tomcat manager 或 actuator 暴露到外网,确实会导致安全事故。在代理层阻止最可靠。
location ~ ^/(manager|host-manager)/ { return 403; }
location /actuator/ {
allow 10.0.0.0/8;
deny all;
proxy_pass http://127.0.0.1:8080;
}
这里的要点是:应用配置也能限制访问,但在代理层限制,无需部署应用即可修改策略。 即使周五下午收到安全检查整改要求,也只需 reload。
access log 应记录什么
默认 combined 格式不足以分析故障,至少要加入以下四项。
log_format labhub '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'ua="$upstream_addr" us=$upstream_status '
'urt=$upstream_response_time rt=$request_time';
$upstream_addr——请求去了哪个后端,负载均衡时必需$upstream_status——后端返回的状态码,可与 nginx 自己生成的 502 区分$upstream_response_time——后端耗时$request_time——从客户端视角计算的总时间
如果 $request_time 很大,而 $upstream_response_time 很小,问题不在后端,而在客户端网络或响应传输。仅凭这两个值的差异,就能多次纠正“后端很慢”的误判。
在实际项目中
缺少六行请求头时,出现的症状看似互不相关:访问日志中的客户端 IP 全都变成同一个代理 IP(缺少 X-Real-IP、X-Forwarded-For);登录后的重定向跳到内部地址或 http(缺少 Host、X-Forwarded-Proto);基于 IP 的访问控制和审计日志整体失去意义。
共同点是平时完全没有问题。因此,往往上线数周后,直到安全审计或故障分析才发现,而届时已经无法靠既有日志追踪任何事情。
proxy_pass 的斜杠更加安静。末尾有斜杠,就删除 location 路径后转发;没有则保留。开发环境使用根路径时差异不明显,到了带 context 路径的生产环境才突然出现 404。