应使用httputil.NewSingleHostReverseProxy配合自定义http.Transport实现HTTPS反向代理,而非直接操作crypto/tls;后者仅负责TLS握手,而代理逻辑(头透传、连接复用等)由ReverseProxy完成,手动建连易导致400/502错误。

反向代理不直接用 crypto/tls,而是用 httputil.NewSingleHostReverseProxy + 自定义 Transport
crypto/tls 本身不处理 HTTP 代理逻辑,它只负责 TLS 握手和加密通道。反向代理的请求转发、头透传、连接复用等由 httputil.NewSingleHostReverseProxy 完成;而目标服务是 HTTPS 时,才需要通过 http.Transport 配置 tls.Config 来控制 TLS 行为。
常见错误是试图用 crypto/tls 手动建连后自己读写 HTTP 请求——这绕过了 ReverseProxy 的 hop-by-hop 头过滤、重定向处理、Body 重放等关键逻辑,极易导致 400/502/空响应。
-
ReverseProxy内部会调用Transport.RoundTrip()发起下游请求,你只需替换其Transport - 若目标是
https://backend.example.com,必须设置Transport.TLSClientConfig,否则默认使用 insecure 配置(Go 1.19+ 已收紧) - 不要在
Director中修改req.URL.Scheme为https后就以为自动走 TLS —— Scheme 只影响 URL 解析,实际是否加密取决于 Transport 是否配置了 TLS
HTTPS 后端转发必须显式配置 Transport.TLSClientConfig
默认 http.Transport 对 HTTPS 目标使用系统根证书 + 安全校验,但很多内网或测试环境后端用自签名证书,此时需定制 tls.Config。关键是:不能只设 InsecureSkipVerify: true,还要注意 SNI 和证书匹配。
- 务必设置
TLSClientConfig.ServerName(或让req.URL.Host包含域名),否则 TLS 握手时 SNI 字段为空,后端可能拒绝连接 - 若后端证书 CN 或 SAN 不含 IP 地址,而你用
https://10.0.0.1访问,必须在tls.Config中用VerifyPeerCertificate手动绕过主机名检查,而非仅靠InsecureSkipVerify - Go 1.21+ 默认禁用 TLS 1.0/1.1,如需兼容老旧后端,得显式设
MinVersion: tls.VersionTLS10,但这是安全降级,应避免
多域名后端或中间证书链要拼进 fullchain.pem
当反向代理要连多个不同域名的 HTTPS 后端(比如 api.example.com 和 files.internal),且每个后端证书由不同 CA 签发,Transport 无法自动切换证书——它只有一套 tls.Config。此时要么:
立即学习“go语言免费学习笔记(深入)”;
- 统一用可信根证书(如系统 CA 或自建私有 CA),让所有后端证书都可被验证
- 用
tls.Config.RootCAs加载自定义*x509.CertPool,把各后端的根/中间证书都加进去 - 若某个后端提供的是带中间证书的完整链,确保该 PEM 文件是
leaf.crt+intermediate.crt拼接顺序(不可颠倒),命名为fullchain.pem,再通过tls.LoadX509KeyPair("fullchain.pem", "key.pem")加载
错误做法:把中间证书单独调用 tls.LoadX509KeyPair,会报 certificate doesn't match private key —— 中间证书没有对应私钥,也不该有。
ModifyResponse 中读取 Body 必须重置 Body,且不能忽略 Close
想在响应返回前查看或修改 Body(如注入 header、重写 HTML),要用 ReverseProxy.ModifyResponse 回调。但 Go 的 http.Response.Body 是一次性流,ioutil.ReadAll 读完后原 Body 就 EOF 了,不重置会导致客户端收不到内容。
- 必须先
defer res.Body.Close(),再ioutil.ReadAll(res.Body),否则连接泄漏 - 读完后要重建 Body:
res.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes))(Go 1.16+ 推荐用io.NopCloser+bytes.NewReader) - 若 Body 很大(如文件下载),避免全量读入内存,改用
io.TeeReader或流式处理 - 不要在
ModifyResponse中修改res.Header后忘记调用res.Header.Set("Content-Length", ...),否则 chunked 编码可能出错
最易被忽略的是:Transport 的 TLS 配置一旦生效,就作用于所有下游 HTTPS 请求;如果多个后端安全要求不一致(一个要严格校验,一个接受自签名),只能拆成多个 ReverseProxy 实例,各自配独立 Transport —— 没有“按域名动态切 tls.Config”的内置机制。


















