侧边栏壁纸
博主头像
一笑痕

人生若只如初见,
是可喜亦或者是可悲?

  • 累计撰写 218 篇文章
  • 累计收到 7 条评论

同一个 302,同源保住 Authorization,换个端口就没了

2026-10-20 / 0 评论 / 7 阅读

同一个 302,同源保住 Authorization,换个端口就没了

同一个 Authorization 头,19 个字符,同源 302 之后一个不少地到了下一个请求,把目标换成另一个端口,到达时是 0 个字符。中间没有报错,客户端拿到的状态码是 200,只是第二个请求的身份没了。

本机起了两个 HTTP 服务,都监听 127.0.0.1,只差端口。一个负责发 302,一个负责记录收到的头。同一份请求头送进去,重定向的去向改一下,落地的结果就分岔。

== 1 同源重定向 ==
== 302 同源 /hop → /final ==
  状态码 200,第二次请求到达 1 个
  authorization 原值到达: true(19 字符)
  x-api-key 原值到达: true(5 字符)
  cookie 原值到达: true(7 字符)

== 2 换端口就算跨源 ==
== 302 127.0.0.1:A → 127.0.0.1:B ==
  状态码 200,第二次请求到达 1 个
  authorization 原值到达: false(0 字符)
  x-api-key 原值到达: true(5 字符)
  cookie 原值到达: false(0 字符)

== 3 只换主机名也算跨源 ==
== 302 127.0.0.1 → localhost ==
  状态码 200,第二次请求到达 1 个
  authorization 原值到达: false(0 字符)
  x-api-key 原值到达: true(5 字符)
  cookie 原值到达: false(0 字符)

== 4 换状态码不改变结论 ==
== 307 跨端口 ==
  状态码 200,第二次请求到达 1 个
  authorization 原值到达: false(0 字符)
  x-api-key 原值到达: true(5 字符)
  cookie 原值到达: false(0 字符)

== 308 跨端口 ==
  状态码 200,第二次请求到达 1 个
  authorization 原值到达: false(0 字符)
  x-api-key 原值到达: true(5 字符)
  cookie 原值到达: false(0 字符)

== 5 不跟随时不发第二个请求 ==
  status=302,location 有值=true,第二个请求数=0

三行对应三个头。同源那一行,Authorization、Cookie 和 x-api-key 全部原值到达。换端口之后,前两个变成 0 个字符,x-api-key 还是 5 个字符。换主机名跟换端口是一个结果:在客户端眼里 127.0.0.1 和 localhost 是两个不同的源,跳过去就算跨源。

Cookie 和 Authorization 一起消失

这两个头是一起走的,Proxy-Authorization 也在同一条规则里。Fetch 规范对跨源重定向的处理写得很直接:不再带 Authorization,Cookie 和 Proxy-Authorization 同样删掉。不用服务端声明什么,客户端自己动手。

规则背后的理由是重定向目标不可信。一个 302 可以把请求指到任何地方,如果凭据跟着走,谁控制了跳转谁就拿到了 token。删掉之后,即使跳转地址被人改过,第二个请求也只是个匿名请求。

删的只是这几个名字。x-api-key 这类自定义头照旧带过去,5 个字符原样到达。这条差别在排查时最容易骗人:抓包看到自定义头还在,就以为鉴权信息也带了,实际服务端认的是 Authorization,它早就不在了。

307 与 308 也一样

换状态码不改变这条规则。302、307、308 三种都在跨端口场景下试过,Authorization 都是 0 个字符。307 与 308 保住的是方法和请求体,不是凭据。

真正的分界在跨不跨源,不在跳转用的是哪个码。

目标端只看到 401

把第二个服务改成一个要求 Bearer 的接口,重定向过去之后,客户端拿到的是 401 和一句 missing token。

== 6 目标端要求 Bearer 时 ==
  跨端口 302 之后,客户端拿到 401,body=missing token
  (同源一条路径下,/api 在同一个端口,另测)
== 7 手工补头之后 ==
  手动取 location 再发一次,authorization 原值到达: true

== 8 说明 ==
  Node v26.8.1,两个服务都在 127.0.0.1 上,只差端口或主机名
  浏览器侧未实测,此处只有 Node fetch 的结果

这条链路最难查的地方在于两边日志各自都正常。第一个服务发了 302,第二个服务收到了请求,只是没带头。前端看到 401 会去怀疑 token 过期,后端看到请求已达会认为网关没转发头,实际是客户端按规范删掉了。

手工跟一次就恢复:把 redirect 设成 manual,自己读出 location 再发第二个请求,Authorization 原值到达。代价是跳转链和循环上限都得自己收拾,302 之后要不要把 POST 改成 GET,也得自己定。

更稳的改法是换掉那条跳转

手工补头能跑,但我倾向于不这么做。凭据跟着跳转走,等于把 token 交给下一个地址,规范删掉它是有道理的,绕过之后这个风险又回来了。

线上常见的情形是鉴权接口把请求 302 到一个静态资源域,或者把 API 域名跳到 CDN。改法在服务端:需要鉴权的接口直接返回数据,别跳转;必须跳的时候,签发一个短期的、只对目标地址有效的参数放在 URL 里,不占 Authorization。回调地址能配置的话,把它固定成同源路径,跳转就不用跨源了。

redirect: 'manual' 留给调试。生产代码里这么做,等于自己重写一遍跟随逻辑,还得保证每一跳都只对可信地址补头。

一条没测的边界

浏览器侧的结果没有实测。规范里对跨源重定向删凭据的要求对浏览器同样有效,但各家实现对 307 与预检的组合有细节差别,这一层只在 Node 上验过,别的客户端不下结论。要确认浏览器行为,得在真实浏览器里看服务端日志,光看 DevTools 的请求列表不够。

    🤞 分享