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 | 永久 | 保持原方法 | 会 |
四条纪律:
- 临时跳转绝不用 301。浏览器会把 301 缓存到 “清缓存也未必消失” 的程度,上线后想改回来只能换 URL。
- POST 跳转用 307/308(保方法)或 303(明确改 GET),不要用 301/302 猜。
- 跳转目标必须白名单,否则是开放重定向漏洞(
?next=https://evil.com)——攻击者可拿你的域名做钓鱼跳板。 - 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 握手在中间层失败。代理应消费这些头并按自己的协议重新生成。
关联
- 前置:8.8 NAT 与端口映射、8.10 反向代理与转发策略
- 并列:8.6 TLS 握手、证书与 mTLS
- 下游:47.1 反向代理、47.4 鉴权与限流应放在哪一层
- 待补:73.5 前端发布与灰度