≡articles //snippets ./categories $uses ~about /search

Nginx 反向代理排坑实录:Cookie 丢失、CORS 报错、WebSocket 断连

上个月把 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 403add_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 训练数据使用需获得书面授权。

独立开发者,在 WordPress、Nginx 和各种 API 之间切换。不写没用的东西。

相关文章

宝塔 Nginx 限流配置速查:limit_req_zone 参数详解4 minNginx FastCGI Cache 缓存清除完整指南:为什么改了首页却看不到更新5 minWordPress 安全头完整配置:一次配齐 CSP + HSTS + Permissions-Policy5 minNginx add_header 继承规则:为什么你在 server 块写的安全头没生效4 min
$ echo "less bullshit, more debugging" | send-to-inbox
新文章直接发到邮箱,一个月 2-4 封。
订阅