上个月把 sodebug.com 的前端从直连 PHP-FPM 切成 Nginx 反向代理,前面放一个轻量 Nginx 做反向代理,后面跑实际的 PHP 服务。切完之后,三个问题接踵而来:登录态频繁丢失、前端跨域请求全部 403、WebSocket 连上就断。
这篇文章是这三个问题的完整排查记录。不是“加上这行配置就好了”——是从抓包到根因再到修复的全链路。
环境:反向代理架构
先说清楚拓扑。反代服务器跑在 443 端口,后端 PHP-FPM 通过内网 127.0.0.1:8080 通信:
# /etc/nginx/sites-available/sodebug — 反代入口
server {
listen 443 ssl;
server_name sodebug.com;
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;
}
}
# 后端 8080 — 实际 WordPress
server {
listen 8080;
server_name sodebug.com;
root /www/wwwroot/sodebug.com;
index index.php;
location ~ .php$ {
fastcgi_pass unix:/tmp/php83-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
看起来很标准的配置。问题就从这里开始。
第一个坑:登录后刷新页面,又变回未登录
用户反馈:登录 WordPress 后台,操作几分钟后刷新页面,又跳回登录页。Cookie 看起来设置成功了,但后续请求中没带上。
先用 curl 模拟一次完整的登录流程,抓 Set-Cookie 和后续请求的 Cookie 头:
$ curl -v -c /tmp/cookies.txt -b /tmp/cookies.txt
-d 'log=admin&pwd=xxxxx&wp-submit=Log+In'
https://sodebug.com/wp-login.php 2>&1 | grep -E 'Set-Cookie|^< HTTP'
< HTTP/2 302
< set-cookie: wordpress_sec_abc123=xxx; path=/wp-admin; domain=sodebug.com; HttpOnly
< set-cookie: wordpress_logged_in_abc123=xxx; path=/; domain=sodebug.com; HttpOnly
# 再用这个 cookie 访问后台
$ curl -b /tmp/cookies.txt https://sodebug.com/wp-admin/ 2>&1 | grep 'Set-Cookie'
# 没有 Set-Cookie 返回——说明后端认为 Cookie 无效
登录成功拿到了 Cookie,但后续请求后端不认。tcpdump 抓一下反代和后端之间的通信:
$ sudo tcpdump -i lo -A port 8080 | grep -i 'wordpress_logged_in'
Cookie: wordpress_logged_in_abc123=xxx
Host: 127.0.0.1:8080
反代把 Host 头正确传了,但 WordPress 看到的 $_SERVER['HTTP_HOST'] 是后端的 127.0.0.1:8080。WordPress 的 Cookie 校验依赖 HTTP_HOST 来匹配 siteurl,当 WP 检测到127.0.0.1:8080 不等于 sodebug.com 时,Cookie 被判定为无效。
检查后端 Nginx 的 fastcgi_params:
$ grep HTTP_HOST /etc/nginx/fastcgi_params
fastcgi_param HTTP_HOST $http_host;
$http_host 取的是请求头 Host 的原始值。反代确实设置了这个头,但关键在,WordPress 需要的不仅是 HTTP_HOST 正确,还需要 $_SERVER['SERVER_NAME'] 也匹配。在后端 Nginx 配置中,SERVER_NAME 来自 server_name 指令:
$ curl -H 'Host: sodebug.com' http://127.0.0.1:8080/wp-login.php 2>&1
| grep -o 'SERVER_NAME.*'
# 后端返回的 PHP 环境变量中,SERVER_NAME = _ (因为server_name指令未匹配)
$ curl -v -H 'Host: sodebug.com' http://127.0.0.1:8080/ 2>&1 | grep '< HTTP'
< HTTP/1.1 200 OK
因为后端 server_name 设的是 sodebug.com 但请求走的是 http://127.0.0.1:8080,Nginx 默认用第一个 server 块处理,SERVER_NAME 取不到正确的值。WordPress 用 $_SERVER['SERVER_NAME'] 拼 Cookie 域名时出错。
修复:proxy_set_header Host 之外还要修后端 server_name
# 后端 server 块 — 加 default_server
server {
listen 8080 default_server;
server_name sodebug.com;
# ... 其余不变
}
验证:
$ curl -v -c /tmp/cookies.txt -b /tmp/cookies.txt
-d 'log=admin&pwd=xxxxx&wp-submit=Log+In'
https://sodebug.com/wp-login.php 2>&1 | grep 'Set-Cookie'
< set-cookie: wordpress_logged_in_abc123=xxx; path=/; domain=sodebug.com; HttpOnly
$ curl -b /tmp/cookies.txt https://sodebug.com/wp-admin/ 2>&1 | grep '<title>'
<title>仪表盘 ‹ SoDebug — WordPress</title>
登录态正常了。核心问题不是 proxy_set_header 没传对——是后端 Nginx 的 server_name 匹配逻辑导致 SERVER_NAME 变量取不到正确值,WordPress 的 Cookie domain 校验失败。
第二个坑:CORS 报错——前端 API 请求全部 403
WordPress REST API 要从前端 JavaScript 调用。浏览器发 preflight 请求(OPTIONS)时报错:
$ curl -v -X OPTIONS
-H 'Origin: https://gkmix.com'
-H 'Access-Control-Request-Method: GET'
https://sodebug.com/wp-json/wp/v2/posts 2>&1 | grep -E 'Access-Control|< HTTP'
< HTTP/2 403
# 没有任何 Access-Control-* 头返回
反代配置里明明加了 CORS 头:
# 反代 Nginx 配置 — 看起来没问题
location /wp-json/ {
add_header Access-Control-Allow-Origin 'https://gkmix.com' always;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS' always;
add_header Access-Control-Allow-Headers 'Authorization, Content-Type' always;
if ($request_method = OPTIONS) {
return 204;
}
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
}
加了 always 还是没返回 CORS 头。检查 Nginx 错误日志:
$ tail -20 /var/log/nginx/sodebug-error.log
2026/07/07 14:23:11 [error] 12345#0: *9821 access forbidden by rule, client: x.x.x.x
“access forbidden by rule”——不是 CORS 配置有问题,是请求在到达 add_header 之前就被拒绝了。检查后端的限制配置:
$ grep -n 'deny|allow|return 403' /etc/nginx/sites-available/sodebug
# 后端 server 块中有安全规则
location ~ .php$ {
# ...
if ($http_origin !~* ^https?://sodebug.com$) {
return 403;
}
}
后端 server 块里有一个 Origin 检查规则,直接 403 了所有非同源请求。反代加 CORS 头时请求已经经过了后端的安全拦截,CORS 头永远不会被看到。
更隐蔽的问题:即使去掉后端的 403 规则,CORS 头还是可能出不来。因为 add_header 指令只在当前层级生效,如果请求匹配到 location ~ .php$,那个 location 里的 fastcgi 配置会重置响应头,反代层的 add_header 就被覆盖了。
$ curl -v -X OPTIONS
-H 'Origin: https://gkmix.com'
-H 'Access-Control-Request-Method: GET'
http://127.0.0.1:8080/wp-json/wp/v2/posts 2>&1 | grep 'Access-Control'
# 直接访问后端,不经过反代——依然没有 CORS 头
修复:CORS 配置放在后端,不依赖反代层
# 后端 server 块 — 直接处理 CORS
location /wp-json/ {
# 移除 Origin 检查规则
add_header Access-Control-Allow-Origin 'https://gkmix.com' always;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS, PUT, DELETE' always;
add_header Access-Control-Allow-Headers 'Authorization, Content-Type, X-WP-Nonce' always;
if ($request_method = OPTIONS) {
add_header Access-Control-Max-Age 86400;
return 204;
}
try_files $uri $uri/ /index.php?$args;
}
验证:
$ curl -v -X OPTIONS
-H 'Origin: https://gkmix.com'
-H 'Access-Control-Request-Method: GET'
https://sodebug.com/wp-json/wp/v2/posts 2>&1 | grep -E 'Access-Control|< HTTP'
< HTTP/2 204
< Access-Control-Allow-Origin: https://gkmix.com
< Access-Control-Allow-Methods: GET, POST, OPTIONS, PUT, DELETE
< Access-Control-Allow-Headers: Authorization, Content-Type, X-WP-Nonce
< Access-Control-Max-Age: 86400
CORS 头正常返回。规则:CORS 配置放在实际处理请求的 server 块里,不要放在反代层。反代层的 add_header 在子请求中大概率被覆盖。
第三个坑:WebSocket 连上 30 秒准时断开
WordPress 后台用到了 WebSocket(部分插件如 Fluent Forms 的实时通知)。连上后准时在第 60 秒断开,重连、再断,周期性。
$ wscat -c wss://sodebug.com/wp-admin/admin-ajax.php?action=xxx 2>&1
Connected (press CTRL+C to quit)
# ... 正常工作 ...
Disconnected (code: 1006, reason: )
# 精确计算断开时间
$ echo "$(date +%s) - $(date -d 'Disconnected之前的那条消息' +%s)" | bc
60
60 秒和超时配置吻合。检查反代和后端的 proxy 相关超时:
$ grep -E 'proxy_read_timeout|proxy_send_timeout|keepalive_timeout'
/etc/nginx/sites-available/sodebug
proxy_read_timeout 60s;
proxy_send_timeout 60s;
keepalive_timeout 65;
proxy_read_timeout 60s,Nginx 反向代理在 60 秒内没收到后端数据就断开连接。WebSocket 长连接在空闲时没有数据传输,60 秒一到就被反代掐断。更致命的是,反代配置没有声明 Upgrade 相关的头:
$ curl -v -H 'Upgrade: websocket' -H 'Connection: Upgrade'
https://sodebug.com/wp-admin/admin-ajax.php 2>&1 | grep -E 'Upgrade|Connection|< HTTP'
< HTTP/2 200
# Connection 头没有返回 Upgrade,说明反代没有转发 WebSocket 握手
WebSocket 握手需要三个条件同时满足:
1) 反代层转发 Upgrade 和 Connection 头;
2) 反代层把 HTTP/1.1 协议升级要求传给后端;
3) 超时时间足够长,不对空闲连接做断开。
修复:WebSocket 专用 location + 超时覆盖
# 反代入口 — WebSocket 专用块
location /wp-admin/admin-ajax.php {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 86400s; # 24 小时,WebSocket 不断
proxy_send_timeout 86400s;
}
关键点:
proxy_http_version 1.1,HTTP/1.1 才支持协议升级,默认的 1.0 不行proxy_set_header Upgrade $http_upgrade,把客户端发来的 Upgrade 头原样传给后端proxy_set_header Connection "upgrade",必须小写"upgrade",不是"Upgrade"。Nginx 的 Connection 头处理区分大小写
验证:
$ curl -v -H 'Upgrade: websocket' -H 'Connection: Upgrade'
https://sodebug.com/wp-admin/admin-ajax.php 2>&1 | grep -E 'Upgrade|101'
< HTTP/1.1 101 Switching Protocols
< Upgrade: websocket
< Connection: Upgrade
101 Switching Protocols,WebSocket 握手成功。再跑 wscat 测试长时间连接:
$ wscat -c wss://sodebug.com/wp-admin/admin-ajax.php?action=xxx
Connected (press CTRL+C to quit)
# 10 分钟后...
# 依然连接正常,无断开
完整反代配置模板
把三个坑的修复合并到一起,这是 sodebug.com 最终的反代配置,去掉注释后不到 40 行:
# === 反代入口 (443) ===
server {
listen 443 ssl http2;
server_name sodebug.com;
# WebSocket 专属
location /wp-admin/admin-ajax.php {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 86400s;
}
# REST API
location /wp-json/ {
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;
}
# 默认转发
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;
}
}
# === 后端 (8080) ===
server {
listen 8080 default_server;
server_name sodebug.com;
root /www/wwwroot/sodebug.com;
# CORS 放在后端处理
location /wp-json/ {
add_header Access-Control-Allow-Origin 'https://gkmix.com' always;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS, PUT, DELETE' always;
add_header Access-Control-Allow-Headers 'Authorization, Content-Type, X-WP-Nonce' always;
if ($request_method = OPTIONS) {
add_header Access-Control-Max-Age 86400;
return 204;
}
try_files $uri $uri/ /index.php?$args;
}
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ .php$ {
fastcgi_pass unix:/tmp/php83-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
总结
三个问题,三个不同的根因层次:
| 症状 | 表面原因 | 真正的根因 | 修复 |
|---|---|---|---|
| Cookie 丢失 | proxy_set_header Host 没设 | 后端 server_name 不匹配 → SERVER_NAME 变量错误 → WP Cookie domain 校验失败 | 后端加 default_server |
| CORS 403 | add_header 没生效 | 后端 location 层的安全规则在 add_header 之前拦截;子请求覆盖反代层的响应头 | CORS 放在后端处理请求的 server 块 |
| WebSocket 断开 | proxy_read_timeout 太短 | 没传 Upgrade/Connection 头;HTTP/1.0 不支持协议升级 | WebSocket 专用 location + HTTP/1.1 + Upgrade 头 |
反向代理的坑很少是配置语法错误:大部分是请求上下文在跨越代理层时丢失或变形:域名变成 IP、协议版本降级、响应头被覆盖。排查的核心思路不是猜,是用 curl -v 和 tcpdump 在每一层分别抓包,对比请求进去和出来时的差异。
如果你也在用类似的VPS 部署架构,或者遇到了 Nginx 限流导致的反代中断,这两篇文章里有一些相关的排查逻辑可以参考。关于反代的缓存层,可以看之前写的 FastCGI Cache 清除指南。反向代理的官方文档在 nginx.org ngx_http_proxy_module,大部分坑的答案其实都在那几页文档里。
© 2026 本文由 Charlie 原创撰写,发布于 sodebug.com。未经授权禁止转载、洗稿、机器抓取。AI 训练数据使用需获得书面授权。