反向代理下的重定向:Location 为什么会变成 http://host:8081/

登录成功却跳到一个打不开的地址,Location 里带着内网端口甚至 localhost。这类问题几乎都出在三个地方:绝对 Location、漏传 X-Forwarded-*、以及 location 补尾斜杠产生的 301。

「接口文档里写的地址是 https://site.example,为什么浏览器跳过去变成了 http://site.example:8081/xxx/?」

这类问题在多层反向代理的环境里出现频率极高,而且代码看起来完全没问题。原因基本都出在三个地方:绝对 Location、漏传 X-Forwarded-*、以及 nginx 给地址补尾斜杠产生的隐藏 301。

一、相对 Location 与绝对 Location

HTTP 的 Location 允许两种写法:

Location: /console              ← 相对,浏览器按当前 origin 解析
Location: https://site.example/console   ← 绝对,原样使用

相对路径的好处是后端不需要知道自己在哪个域名、哪个端口后面。缺点是有些框架/场景不方便——比如从 http 跳到 https 时必须给绝对地址。

反向代理下的原则很简单:

能用相对就用相对;必须用绝对时,绝对地址必须来自配置,不能来自请求头推导。

二、后端为什么会「自己拼」绝对地址

大部分框架在生成绝对 URL 时,会看两个东西:Host 头和 X-Forwarded-* 系列。代理层只要有一个没传对,就会拼出内网地址。

头 作用 漏了会怎样
Host 对外域名与端口 生成 http://localhost:3000/...
X-Forwarded-Proto 原始协议 https 站点生成 http://...
X-Forwarded-Host 原始 host(部分框架优先读它) 生成内网 host
X-Forwarded-For 客户端真实 IP 日志与限流全部记成代理 IP
X-Forwarded-Port 原始端口 生成带内网端口的地址

一份够用的代理头配置:

location / {
    proxy_pass http://127.0.0.1:3002;
    proxy_http_version 1.1;
    proxy_set_header Host              $host;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header X-Forwarded-For   $remote_addr;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header X-Forwarded-Port  443;
}

X-Forwarded-For 用 $remote_addr 还是 $proxy_add_x_forwarded_for,取决于你是「覆盖式传出」还是「追加」。入口层应该是覆盖式:客户端自己带进来的 XFF 不可信,直接丢弃。

三、nginx 的两类隐藏 301

1. 补尾斜杠

当 location 是 /admin/ 这种以斜杠结尾的前缀,而请求是 /admin 时,nginx 会:

  • 先尝试用 location / 处理,如果命中的是目录/代理,往往由后端决定;
  • 但如果命中的是 root/alias 指向目录,nginx 会主动 301 到 /admin/。

而 301 的 Location 是绝对地址,由 $scheme、$host、server_port 拼出来。中间层如果 host 被改写成 ispt.top:8081,就会得到:

HTTP/1.1 301 Moved Permanently
Location: http://ispt.top:8081/admin/

浏览器去连 8081,而 8081 只监听 127.0.0.1——「找不到网页」。

2. proxy_pass 带不带 URI

这一条不产生 301,但会让跳转「看起来没生效」:

# 不带 URI:原样转发,/gitea/foo → 后端收到 /gitea/foo
location /gitea/ { proxy_pass http://127.0.0.1:23000; }

# 带 URI:前缀被替换,/gitea/foo → 后端收到 /foo
location /gitea/ { proxy_pass http://127.0.0.1:23000/; }

差别就在结尾那个 /。后端如果是按根路径设计的(比如 Gitea、Portainer),必须用第二种;否则所有静态资源都会 404。

四、这次的真实链路

站点是四层结构,每一层都可能改写地址:

浏览器
  │  https://ispt.top/console
  ▼
443 入口(TLS 终结)      proxy_set_header Host $http_host;
  │                       X-Forwarded-Proto https
  ▼
8080 WAF(BunkerWeb)
  │
  ▼
8081 安全层(nginx)      ← 这里配置了约 30 条 location
  │                       Host 用的是 $host,port 丢了
  ▼
ispt-app:3002(Next.js)  ← 303 Location: /console

问题出在 8081:它自己认为「我叫 ispt.top:8081」,所以任何需要补全的绝对地址都带着 :8081。而 8081 对外不可见。

修复分两步:

  1. 应用侧不再产生绝对地址:登录成功返回 303 Location: /console,由浏览器按当前 origin 解析;
  2. 管理端的对外入口收口到环境变量:如果确实需要给绝对地址(比如发邮件里的链接、/studio 兼容跳转),只读 ADMIN_PUBLIC_URL。
const ADMIN_URL = process.env.ADMIN_PUBLIC_URL || '/console';

五、排查方法

1. 只看一跳,不要跟随

curl -sI https://site.example/login | grep -iE '^(HTTP|location)'

想看完整链路再用 curl -sIL,把所有跳转打印出来:

curl -sIL https://site.example/admin 2>&1 | grep -iE '^(HTTP|location)'

2. 确认哪一层在改写

按顺序对每一层直连(都在服务器上跑):

curl -sI http://127.0.0.1:3002/admin   | grep -i location   # 应用本身
curl -sI http://127.0.0.1:8081/admin   | grep -i location   # 安全层
curl -sI http://127.0.0.1:8080/admin   | grep -i location   # WAF
curl -sI https://site.example/admin    | grep -i location   # 入口

哪一层的 Location 开始变样,问题就在那一层。

3. 别用 -L 调试

curl -L 会自动跟随,最后只看到终点的状态码,中间所有 301/302 的信息都丢了。排查跳转问题的第一步永远是关掉跟随。

六、工程上的收口

三条规则,能挡掉绝大多数同类问题:

  1. 重定向只写相对路径。后端不知道自己在几层代理后面,猜就是错。
  2. 绝对地址只有一个来源:环境变量。换域名、加前缀、临时走端口时只改配置,不改代码。
  3. 入口层统一注入代理头,下游层不要各自 proxy_set_header Host $host 反复改写——每改一次就丢一点原始信息。

小结

  • Location 能用相对就别用绝对;绝对地址必须来自配置;
  • 反向代理下出现内网 IP、内网端口、localhost 的 URL,先查 Host 与 X-Forwarded-*;
  • nginx 给 location /X/ 补尾斜杠产生的 301 是绝对地址,这是最常见的「莫名多出个端口」来源;
  • proxy_pass 结尾那个斜杠决定前缀是否被剥掉,静态资源 404 优先怀疑它;
  • 排查永远从「逐层直连 + 只看一跳」开始,不要靠猜。
查看「工程效能」全部文章 →

同分类文章

查看 0 评论 0

评论

正在加载评论…

留言提交后自动审核:正常内容立即显示,含敏感信息的会转人工复核。

反向代理下的重定向:Location 为什么会变成 http://host:8081/ - 起航社区