当前位置:首页 > 文章列表 > Golang > Go教程 > Go httputil.ReverseProxy.Rewrite 怎样重写转发请求

Go httputil.ReverseProxy.Rewrite 怎样重写转发请求

来源:17golang原创 2026-10-26 15:38:30 0浏览 收藏

我第一次把 Go 反向代理接进一个已有 HTTP 服务时,真正卡住的不是“怎么把请求转发出去”,而是改写对象经常选错:路由要改目标地址,Host 又要不要保留,客户端带来的 X-Forwarded-For 能不能直接信任。现在更稳妥的做法是直接使用 httputil.ReverseProxy 的 Rewrite,把入站请求和出站请求分开看。

官方资料:https://pkg.go.dev/net/http/httputil#ReverseProxy

下面用一个很小的代理项目串起完整写法:客户端访问代理,代理把路径转到后端服务,并由 Rewrite 统一处理目标 URL、Host 和转发头。这里的重点是代码边界和使用条件,不把示例当成已经在某台机器上运行过的证据。

先确定这个小项目要改什么

项目假设后端服务位于 http://127.0.0.1:9000,代理监听 :8080。客户端请求 /api/users?id=7 时,代理把它送到后端的 /internal/api/users?id=7。

在 Rewrite 回调中,ProxyRequest.In 表示收到的原始请求,应该把它当作只读输入;ProxyRequest.Out 才是准备交给 Transport 的出站请求,可以修改,也可以替换。这个区分是后面所有代码的基础。

入站请求、出站请求、Rewrite、SetURL、Transport 和后端服务的静态关系图
图1:Rewrite 改写入站请求与出站请求边界的静态说明图,不是运行截图。

准备目标地址和代理 Handler

先解析固定的后端地址,再构造一个 ReverseProxy。解析失败时直接返回错误,避免把一个不完整的 URL 交给代理。

package main

import (
    "log"
    "net/http"
    "net/http/httputil"
    "net/url"
)

func newProxy() (*httputil.ReverseProxy, error) {
    // 目标地址只描述后端的协议、主机和基础路径。
    target, err := url.Parse("http://127.0.0.1:9000/internal")
    if err != nil {
        return nil, err
    }

    proxy := &httputil.ReverseProxy{
        Rewrite: func(pr *httputil.ProxyRequest) {
            // 后续只改写 Out;In 用来读取原始 Host、协议等信息。
            pr.SetURL(target)
            pr.Out.Host = pr.In.Host
            pr.SetXForwarded()
        },
    }
    return proxy, nil
}

func main() {
    // 代理本身是一个标准 Handler,可以挂到已有路由树。
    proxy, err := newProxy()
    if err != nil {
        log.Fatal(err)
    }
    log.Fatal(http.ListenAndServe(":8080", proxy))
}

Rewrite 和 Director 只能二选一,不能在同一个 ReverseProxy 中同时设置。上面的 SetURL 会把目标的 scheme、host 和基础路径写入出站 URL,并按规则拼接入站路径;它默认还会把出站 Host 改成目标主机,所以示例随后显式保留了客户端请求的 Host。

Rewrite 里到底该改哪个请求

我比较容易犯的错误是把 pr.In.URL 当成“待发送的请求”来改。这样做不仅破坏了对象边界,也让后续代码很难判断哪些信息来自客户端、哪些信息已经被代理重写。更清晰的判断方式只有一句:从 In 读取,向 Out 写入。

Rewrite: func(pr *httputil.ProxyRequest) {
    // In 是原始请求,只读取客户端需要保留的上下文。
    originalHost := pr.In.Host

    // SetURL 修改 Out 的目标协议、主机和基础路径。
    pr.SetURL(target)

    // 需要后端按原始域名路由时,覆盖 SetURL 默认生成的 Host。
    pr.Out.Host = originalHost

    // 回调返回后不要再访问 pr;代理会继续使用 Out 发送请求。
}

官方文档还特别强调,Rewrite 返回后不能继续访问传入的 ProxyRequest 及其内容。因此不要把 pr 保存到异步 goroutine,也不要在回调结束后再读取 pr.Out 作为“日志快照”。如果需要记录信息,应在回调中提取必要字段后保存独立的值。

路径拼接和 Host 的两个取舍

如果目标 URL 是 http://127.0.0.1:9000/internal,入站请求路径是 /api/users,SetURL 的效果是把目标路径和入站路径组合为后端路径。它适合“代理统一挂在某个后端前缀下”的场景。

如果你不希望拼接入站路径,而是希望完全控制出站路径,可以在 Rewrite 中直接给 pr.Out.URL.Path 赋值。但此时要同时考虑 RawPath、路径转义和尾部斜杠,不能只凭字符串拼接就认为所有 URL 都安全。普通反向代理优先用 SetURL,只有路径规则很明确时才手动改写。

Host 也有两种常见意图:

  • 让后端按目标站点识别请求:保留 SetURL 设置的目标 Host。
  • 让后端按用户访问的域名分流:在 SetURL 后设置 pr.Out.Host = pr.In.Host。

这两个选择都可以成立,但必须和后端的虚拟主机、鉴权或租户路由约定一致,不要因为“能转发”就默认其中一个。

Host 和转发头怎样一起处理

使用 Rewrite 时,代理会在回调前移除入站请求中的 Forwarded 和 X-Forwarded-* 头。这样做的好处是客户端不能直接把一组伪造的来源信息原样带到后端。需要建立规范转发信息时,再调用 SetXForwarded。

目标 URL、出站 Host、X-Forwarded-For、X-Forwarded-Host、X-Forwarded-Proto 与后端服务的静态关系图
图2:目标地址、Host 与 X-Forwarded-* 元数据关系的静态说明图,不是运行截图。
Rewrite: func(pr *httputil.ProxyRequest) {
    // 先确定后端路由;SetURL 默认会同步调整出站 Host。
    pr.SetURL(target)

    // 业务按原始域名路由时再保留入站 Host。
    pr.Out.Host = pr.In.Host

    // 根据入站连接重建来源 IP、原始 Host 和协议。
    pr.SetXForwarded()
}

SetXForwarded 会设置 X-Forwarded-For、X-Forwarded-Host 和 X-Forwarded-Proto。如果代理位于另一层受信任代理之后,并且确实需要把已有的 X-Forwarded-For 链继续传下去,应先按信任边界复制入站值,再调用 SetXForwarded;不要无条件相信来自公网客户端的同名头。

Rewrite: func(pr *httputil.ProxyRequest) {
    pr.SetURL(target)

    // 只有确认上游代理可信时,才把它的来源链交给 SetXForwarded 追加。
    trustedChain := pr.In.Header.Get("X-Forwarded-For")
    if trustedChain != "" {
        pr.Out.Header.Set("X-Forwarded-For", trustedChain)
    }
    pr.SetXForwarded()
}

查询参数和 Director 迁移时要留意什么

Rewrite 被调用前,不可解析的查询参数会从出站请求中清理。如果业务必须保留原始的 RawQuery 字符串,可以在回调中从 In.URL.RawQuery 复制到 Out.URL.RawQuery,但要先确认代理与后端对重复键、编码和分隔符的解释一致;否则可能形成参数解析差异。

Rewrite: func(pr *httputil.ProxyRequest) {
    pr.SetURL(target)

    // 仅在上下游明确约定原始查询串语义时才完整复制。
    pr.Out.URL.RawQuery = pr.In.URL.RawQuery
}

从旧代码迁移时,另一个坑是把 Director 的写法原封不动搬过来。官方文档把 Director 标记为弃用方向;它返回后仍可能发生 hop-by-hop header 清理,导致你在 Director 中新增的某些头被移除。使用 Rewrite 可以在预处理完成后修改出站请求,让需要保留的改动处在更合适的位置。

把小项目接入现有服务时的验收清单

把代理挂到真实服务前,我会按下面几项逐一确认:

  1. 目标 URL 能被正确解析,基础路径是否应该参与拼接已经明确。
  2. 回调只从 In 读取,只向 Out 写入,没有把 ProxyRequest 带出回调生命周期。
  3. Rewrite 与 Director 没有同时配置。
  4. Host 的取值符合后端虚拟主机或租户路由约定。
  5. X-Forwarded-For 的来源链有明确的信任边界,未直接接受公网伪造值。
  6. 只有确实需要保留原始查询串时才复制 RawQuery,并记录上下游解析约定。
  7. 需要 WebSocket 或其他协议升级时,额外检查 Transport 与后端能力,不把普通 HTTP 代理示例当成全部配置。

如果只是单一后端、单一基础路径,SetURL 通常已经足够;如果路由规则、Host 规则和来源头规则开始互相交织,就把每一项拆成独立的小函数,让 Rewrite 只负责组装,后续排查会轻松很多。

常见相关问题

Rewrite 可以同时设置 Director 吗?

不可以。两者最多设置一个;同一实例同时设置或同时为空都会让代理无法按预期工作。

SetURL 后为什么后端看到的 Host 变了?

因为 SetURL 默认会把出站 Host 改成目标主机。如果后端需要原始 Host,在 SetURL 后显式设置 pr.Out.Host = pr.In.Host。

能不能在 goroutine 里继续使用 ProxyRequest?

不建议也不应这样做。Rewrite 返回后不能再访问它及其内容;异步逻辑应复制出独立的字符串、数值或 Header 快照。

为什么不直接复制客户端的 X-Forwarded-For?

因为客户端可以伪造它。先定义可信上游,再决定是否复制已有链,并用 SetXForwarded 生成当前代理看到的来源信息。

归纳起来,Rewrite 的核心不是“把 URL 改一下”,而是把入站事实、出站意图和转发元数据分开管理:从 In 取信息,改 Out,用 SetURL 处理常规路由,再明确决定 Host、转发头和查询串的边界。这样一个小型代理才更容易接入已有服务并长期维护。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go netip.Addr.Is4 怎样判断地址是否为 IPv4Go netip.Addr.Is4 怎样判断地址是否为 IPv4
上一篇
Go netip.Addr.Is4 怎样判断地址是否为 IPv4
Go netip.Addr.Compare 怎样稳定排序 IP 地址
下一篇
Go netip.Addr.Compare 怎样稳定排序 IP 地址
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    709次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    715次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    796次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    754次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    588次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码