8.12 代理链、跨域与重定向

一句话

代理链的风险不在 “多一跳”,而在每一跳都可能改写请求头、丢掉源信息、让缓存与重定向语义失效;跨域是浏览器替服务端做的额外校验,重定向则是 “把一个 URL 的解释权交给另一个 URL”——三者都要求你明确 “谁信任谁”。

代理链:三种头,两种信任模型

头 谁写 内容 可信前提
X-Forwarded-For 每跳代理追加 客户端与被经过代理的 IP 列表(逗号分隔) 只信任最后一跳追加的那段(NAT 侧的源地址改写见 8.8 NAT 与端口映射)
Forwarded(RFC 7239) 每跳代理追加 for= / proto= / by= 结构化表达 同上,但支持者较少
Via 每跳代理追加 协议版本与代理标识 用于排障与合规审计,不用于鉴权
客户端 1.2.3.4 → CDN(203.0.113.9) → LB(10.0.0.5) → 应用
应用看到的 XFF:1.2.3.4, 203.0.113.9        ← 第一段是“客户端声称”,第二段是本跳追加

🔑 取 XFF 的正确姿势:不是取第一段,而是从右往左数,跳过你信任的代理数。取第一段等于让攻击者自报身份——他只要在请求里自带 X-Forwarded-For: 8.8.8.8 就能伪造来源 IP。

代理自身的配置与信任边界见 8.10 反向代理与转发策略。逐跳头(hop-by-hop)必须由代理消费、不能透传:Connection、Keep-Alive、Proxy-Authenticate、TE、Trailer、Transfer-Encoding、Upgrade。透传会导致协议错乱(如 Transfer-Encoding: chunked 被重复处理)。

代理链的三个工程后果

后果 表现 处理
源信息丢失 风控/限流/审计看到的是代理 IP 配置转发头 + 应用侧按信任链解析
延迟叠加 每跳增加 1–5ms(含 TLS 终结与缓冲) 减少跳数、合并入口、就近接入
语义被改写 缓存键/重定向/Host 不一致 统一在入口层定义 “真实 Host 与协议”(X-Forwarded-Proto)

最常见的事故是 HTTPS 在入口终结(证书与握手见 8.6 TLS 握手、证书与 mTLS)、应用却按 HTTP 生成绝对 URL,于是重定向跳回 http://,形成 “登录后掉回 http” 的循环。

跨域:浏览器替服务端做的校验

同源 = 协议 + 主机 + 端口,三者任一不同即为跨域。跨域请求能不能发、能不能读响应,由 CORS 头决定:

请求类型 行为 需要的响应头
简单请求(GET/POST + 安全头) 直接发出,浏览器检查响应 Access-Control-Allow-Origin
预检请求(PUT/DELETE、自定义头、JSON) 先发 OPTIONS,通过后才发真实请求 Allow-Origin、Allow-Methods、Allow-Headers
带凭证(cookie / HTTP auth) 需要显式允许 Allow-Origin 不能是 *,且 Allow-Credentials: true
反模式 后果
反射 Origin 头(Allow-Origin: <任意 origin>) 等于对所有站点开放,CSRF 防护失效
Allow-Origin: * + Allow-Credentials: true 浏览器直接拒绝,且说明设计没想清
不配 Access-Control-Max-Age 每个请求都多一次预检往返
用 CORS 当鉴权 CORS 只约束浏览器,服务端到服务端调用完全不受限

预检缓存建议 Max-Age: 600 以上(浏览器上限通常 2 小时),能省掉大量 OPTIONS。

重定向:状态码的语义差别是 “方法会不会变”

状态码 语义 方法 会被浏览器缓存吗
301 永久 可能被改成 GET 会,且很难清除
302 临时(历史实现) 可能被改成 GET 一般不
303 看别处 强制改 GET 一般不
307 临时 保持原方法 一般不
308 永久 保持原方法 会

四条纪律:

  1. 临时跳转绝不用 301。浏览器会把 301 缓存到 “清缓存也未必消失” 的程度,上线后想改回来只能换 URL。
  2. POST 跳转用 307/308(保方法)或 303(明确改 GET),不要用 301/302 猜。
  3. 跳转目标必须白名单,否则是开放重定向漏洞(?next=https://evil.com)——攻击者可拿你的域名做钓鱼跳板。
  4. HTTPS 跳转放在最外层,并配 HSTS(max-age 至少半年,确认全站 HTTPS 后再考虑 preload)。

反模式

反模式 后果
信任 XFF 第一段 来源 IP 可伪造,风控与限流被绕过
透传逐跳头 分块编码被重复处理,出现截断/挂起
应用按 HTTP 生成绝对 URL 入口终结 TLS 后出现重定向循环或降级到 http
用 301 做 A/B 或临时活动页 用户被浏览器永久缓存到旧地址
?next= 直接跳转 开放重定向,钓鱼与绕过登录的常见入口
代理链里每跳都改写 Host 缓存键错乱、证书校验失败、路由命中错误站点
只用 CORS 保护接口 非浏览器客户端(curl/脚本)完全不受约束

三条铁律

🔑 只信任最近一跳:XFF、X-Forwarded-Proto、Host 都按 “信任链” 解析,不按 “第一段”。

🔑 301/308 是承诺:一旦发出就被缓存,临时跳转用 302/307。

🔑 重定向目标必须白名单;?next= 不做校验就是开放重定向。

自检

Q1 应用日志里 90% 的请求 IP 都是同一个地址,怎么定位与修复?

▸ 答案:那个地址是最近一跳代理(LB/CDN)的 IP,说明没有正确解析转发头。修法分两步:① 入口层确认追加 X-Forwarded-For(七层)或开启 PROXY protocol(四层);② 应用侧按 “从右往左、跳过已知代理数” 的规则取值,并把结果落到日志。注意不要取第一段——那可被伪造。

Q2 入口做了 HTTPS 终结,登录后却跳回 http://,为什么?

▸ 答案:因为应用按 “收到的连接协议” 生成绝对 URL,而它到入口这一段是 HTTP。修法是让入口传 X-Forwarded-Proto: https,应用信任并使用它生成 URL;同时在入口层把 HTTP 301 到 HTTPS、加 HSTS,避免用户回到明文。

Q3 为什么说 “用 301 做临时跳转” 是事故级错误?

▸ 答案:因为 301 是永久语义,浏览器会长期缓存(部分浏览器甚至跨会话保留)。做过一次 301,后续改回原地址时老用户仍被带到旧地址,只能换域名或等缓存过期。临时场景要用 302/307;确实永久迁移才用 301/308,并在切换前评估缓存影响。

Q4 CORS 配了 Allow-Origin: * 还带 cookie,为什么请求失败?

▸ 答案:规范禁止 * 与凭证共存:带 credentials: include 时,Access-Control-Allow-Origin 必须是具体的 Origin 且 Allow-Credentials: true。修法是服务端回显白名单内的 Origin(不是反射任意 Origin!),并把 Vary: Origin 加上,避免缓存串用。

Q5 代理链里为什么不能把 Connection、Transfer-Encoding 这类头原样透传?

▸ 答案:它们是逐跳头,只对 “相邻两跳” 有效。透传会让下一跳重复处理:例如上游已解掉分块编码,下游又按 chunked 解析,可能截断或挂起;Connection: Upgrade 透传不当还会让 WebSocket 握手在中间层失败。代理应消费这些头并按自己的协议重新生成。

关联