登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Go HTTP 重定向为什么请求头会消失:CheckRedirect 与敏感头安全边界

来源:17golang原创

时间:2026-07-24 13:31:14 268浏览 收藏

服务端把接口从 /v1/profile 临时迁到 /v2/profile 后,Go 客户端开始收到 401。抓包看起来只是多了一次 302,但真正变化的是第二个请求:请求地址换了,Authorization 没有按预期到达新主机。这个行为不是 Go 随机丢头,而是 http.Client 对重定向请求头有明确的安全复制规则。

这个问题的核心原因不是重定向逻辑故障,而是标准库默认开启的敏感头跨跳转保护,大部分开发者没注意到这个隐式规则,才会出现明明代码没改、迁完环境就鉴权失败的情况。

要点速览

  • 301、302、303、307、308 都可能触发新的请求,不能只看最终状态码。
  • 同一主机的普通请求头通常会被带到新请求,但跨主机时敏感头会被保护。
  • AuthorizationCookie 等凭据不应为了“修复 401”而无条件复制。
  • 需要自定义策略时,用 CheckRedirect 明确允许的目标主机和可复制头,再用测试记录每一跳。

401 不是接口坏了,先把重定向链记录下来

我先把服务端缩成两个 httptest 端点:第一个端点返回 302,Location 指向第二个端点;第二个端点只在收到 Authorization: Bearer demo-token 时返回 200。这样可以把“服务端鉴权失败”和“客户端没有把头带过去”分开。

func TestRedirectHeaders(t *testing.T) {
    var secondAuth string
    target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        secondAuth = r.Header.Get("Authorization")
        if secondAuth != "Bearer demo-token" {
            http.Error(w, "missing auth", http.StatusUnauthorized)
            return
        }
        w.WriteHeader(http.StatusOK)
    }))
    defer target.Close()

    targetURL := strings.Replace(target.URL, "127.0.0.1", "localhost", 1)
    first := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        http.Redirect(w, r, targetURL+"/v2/profile", http.StatusFound)
    }))
    defer first.Close()

    req, _ := http.NewRequest(http.MethodGet, first.URL+"/v1/profile", nil)
    req.Header.Set("Authorization", "Bearer demo-token")
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        t.Fatal(err)
    }
    defer resp.Body.Close()
    t.Logf("status=%d second_auth=%q", resp.StatusCode, secondAuth)
}

这里用 strings.Replace 把目标地址的主机名从 127.0.0.1 改成 localhost,确保测试真的跨了主机名边界。最终响应是 401,日志中的 second_auth 为空,说明问题发生在重定向请求生成阶段,而不是第二个端点误读了头。

Go 的重定向处理到底复制了哪些请求头

http.Client 默认会跟随重定向。它会创建一个新的请求,并根据新旧 URL 的关系决定哪些头可以沿用。常见的同主机跳转通常能保留普通头;当主机发生变化时,凭据类头会受到更严格的限制。

场景默认观察处理建议
同主机,路径变化普通头大多延续仍记录最终 URL 和状态
跨主机,Authorization可能被移除不要无条件补回
跨主机,Cookie按 Cookie 作用域判断交给 CookieJar 和域规则
不允许跟随返回 3xx 响应由业务检查 Location

这里的“可能”很重要:不要凭记忆把所有头都归为“会丢”或“不会丢”。真实结论取决于头名、重定向前后的主机关系、请求方法和客户端配置。生产排查时应该把每一跳的 URL、状态码和允许记录的头名写入日志,绝不要把完整令牌写进日志。

Go HTTP 重定向章节:初始请求经过 302 后到达新主机,Authorization 在安全边界处被移除并返回 401

CheckRedirect 应该控制策略,而不是盲目补头

如果业务明确知道重定向只允许发生在自己的域名集合内,可以在 CheckRedirect 里做目标校验。策略的顺序建议是:先检查目标,再决定是否继续,最后才考虑是否需要补充业务头。

var allowedHosts = map[string]bool{
    "api.example.com":     true,
    "upload.example.com":  true,
}

client := &http.Client{
    CheckRedirect: func(next *http.Request, history []*http.Request) error {
        if !allowedHosts[next.URL.Hostname()] {
            return http.ErrUseLastResponse
        }
        // 只有确认目标属于同一信任边界,才复制业务头。
        if len(history) > 0 {
            if token := history[0].Header.Get("X-Request-Token"); token != "" {
                next.Header.Set("X-Request-Token", token)
            }
        }
        return nil
    },
}

示例故意复制的是业务自定义头,而不是把 Authorization 原样灌给所有目标。即便目标主机在白名单里,也要确认它属于同一租户和同一凭据边界;白名单是必要条件,不是充分条件。

用三组断言把“能跳转”和“能带凭据”分开

排查时最容易漏掉的是只断言最终状态码。更稳的测试要同时记录跳转次数、最终 URL、第二跳收到的头,以及遇到未知主机时客户端是否停在 302。

func TestRedirectPolicy(t *testing.T) {
    var visited []string
    client := &http.Client{
        CheckRedirect: func(next *http.Request, history []*http.Request) error {
            visited = append(visited, next.URL.String())
            if next.URL.Hostname() != "api.example.com" {
                return http.ErrUseLastResponse
            }
            return nil
        },
    }
    _ = client
    // 断言:允许的目标继续;未知目标保留 3xx;日志不包含完整 token。
    _ = visited
}

真实项目中可以给每个请求加一个不敏感的跳转序号,例如 redirect_hops=1,并在测试中检查 resp.Request.URL。如果必须接收 302 而由业务层处理,就使用 http.ErrUseLastResponse,不要把响应体误当作最终业务响应。

CheckRedirect 策略章节:目标主机先经过白名单校验,允许的请求继续,未知主机停在 302

相关问题:修好 401 不能留下凭据外泄

为什么把 Authorization 从 history[0] 复制到 next 不安全?

因为 Location 可能来自配置错误、被篡改的代理或第三方跳转。无条件复制会把当前凭据交给不属于业务信任边界的主机。先校验目标,再按最小权限选择头,通常比“所有头复制一遍”更可靠。

307 和 302 的重定向有什么排查差异?

307、308 更倾向保留原请求方法和请求体;302、303 在常见客户端行为下可能变成 GET。上传、付款、写操作遇到跳转时,除了看请求头,还要检查方法和请求体是否被重新发送。

为什么不建议直接关闭重定向?

关闭自动跳转能让业务看到原始 3xx,但不代表问题消失。适合的做法是针对鉴权、写操作和跨主机目标采用不同策略,并把 Location 纳入允许列表与审计。

上线前的最小检查清单

  • 记录每次重定向的状态码、目标主机和跳转次数,不记录完整 Cookie 或令牌。
  • 为同主机、跨主机、302、307 各写一个测试,分别验证方法、请求体和敏感头。
  • 需要保留业务头时,把目标主机、租户边界和头名写成明确规则。
  • 遇到 401 时先看最终请求的主机和头集合,再决定是否修改服务端鉴权。

判断这类问题的关键不是“Go 会不会丢请求头”,而是把重定向当成一次新的请求边界:地址变了,信任关系也可能变。把目标校验、头复制和回归测试拆开,既能解决迁移期间的 401,也能避免为了追求自动跳转而扩大凭据暴露面。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>