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

HTTP 重定向后认证头丢失的客户端策略

来源:17golang原创

时间:2026-10-10 17:42:10 435浏览 收藏

如果 Go 的 http.Client 请求遇到重定向,后续请求看不到 Authorization,通常不是 Header 被随机清空,而是客户端在保护敏感信息。默认策略会把认证头、Cookie 等敏感头部视为目标主机相关的数据:跳到不属于原域名或子域名的主机时不继续转发;同主机或子域名则可能保留。因此,修复重点不是在回调里无条件补回令牌,而是先判断跳转目标是否仍属于你信任的 Origin。

官方文档:https://pkg.go.dev/net/http

最稳妥的默认方案是:让自动重定向只在 HTTPS 且精确匹配可信主机时继续;如果业务确实要跨主机转交认证,就关闭自动跟随,校验 Location 后为新请求显式设置认证头。

为什么跨主机后 Authorization 会消失

http.Client 会自动处理 301、302、303、307、308,并在发送下一跳前准备一个新的 Request。Go 文档明确说明,Authorization、WWW-Authenticate 和 Cookie 属于敏感头:跳转到原主机或其子域名时可以转发,跳转到另一个主机时默认不会转发。

这里的“子域名”不是“看起来像同一家公司”这么宽松的判断。api.example.com 与 login.example.com 的主机名关系,和 example.com 与 example.com.evil.test 完全不同。对于承载 Bearer Token 的客户端,建议把 Go 的默认判断视为最低限度保护,再叠加自己的精确 allowlist。

Go http.Client 重定向时初始请求、目标主机与 Authorization Cookie 的静态关系图
图1:默认重定向策略的敏感头部边界说明图,不是运行截图或请求日志。

还要注意,认证头是否存在和 HTTP 方法是否改变是两件事。301、302、303 在很多非 GET 请求上会让后续请求变成 GET;307、308 更倾向于保留原方法和请求体,但只有请求具备可重新生成 Body 的 GetBody 时才适合继续。不要看到最终 URL 正确,就假定原请求的语义和头部都原样保留。

CheckRedirect 里的 req 和 via 分别代表什么

设置 Client.CheckRedirect 后,Go 会在真正发送下一跳前调用回调。回调的 req 是即将发送的请求,via 是已经发出的请求列表,顺序从最早的请求开始。也就是说,排查认证头时应先看 req.URL、req.Header 和 via[0].URL,不要只看最初构造的请求对象。

package main

import (
	"fmt"
	"net/http"
)

func traceRedirect() *http.Client {
	return &http.Client{
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			// req 是下一跳,via[0] 是原始请求;只记录主机和是否带头,不打印令牌。
			if len(via) == 0 {
				return nil
			}
			fmt.Printf("redirect %s -> %s, authorization=%t\n",
				via[0].URL.Host, req.URL.Host, req.Header.Get("Authorization") != "")
			return nil
		},
	}
}

这个回调只用于理解边界,不建议把完整的认证值写入日志。若 req.Header.Get("Authorization") 为空,而 req.URL.Host 已经换成另一台主机,通常正好对应默认的敏感头保护。

用 CheckRedirect 建立可信跳转策略

如果业务只允许在同一个 HTTPS Origin 内跳转,可以在回调中比较原始主机和即将访问的主机。这里用精确主机匹配,而不是“同后缀就算可信”,并在不可信时返回错误,让请求失败关闭。

package clientpolicy

import (
	"errors"
	"net/http"
	"strings"
)

var ErrUntrustedRedirect = errors.New("untrusted HTTP redirect")

func NewClient() *http.Client {
	return &http.Client{
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			// 没有上一跳时不应进入这里;保留保护,避免索引越界。
			if len(via) == 0 {
				return nil
			}

			origin := via[0].URL
			// 认证请求只允许从 HTTPS 到 HTTPS,且主机名和端口精确一致。
			if origin.Scheme != "https" || req.URL.Scheme != "https" ||
				!strings.EqualFold(origin.Host, req.URL.Host) {
				return ErrUntrustedRedirect
			}
			return nil
		},
	}
}
CheckRedirect 结合 HTTPS、可信主机白名单和 Authorization 的静态策略关系图
图2:基于可信 Origin 的 CheckRedirect 策略说明图,不是运行截图或安全审计证据。

如果你希望“遇到 302 就拿到响应,不要再发下一跳”,可以返回 http.ErrUseLastResponse。它会让 Do 返回最近一次重定向响应和空错误,但此时响应体保持打开状态,调用方必须负责关闭。若返回普通错误,Go 会停止发送下一跳,并按客户端策略关闭前一个响应体。

确实要跨主机时,先校验再手动转交认证

有些架构确实会从网关跳到登录域、再跳回 API 域。此时不要在 CheckRedirect 里把原始 Authorization 复制到任何新地址。更清晰的做法是关闭自动跟随,读取 Location,只接受明确的 HTTPS 主机白名单,然后为新请求重新设置认证头。

package clientpolicy

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"strings"
)

var ErrRedirectTarget = errors.New("redirect target is not allowed")

func trustedAPI(u *url.URL) bool {
	// 示例域名只代表业务 allowlist;生产代码应从受控配置读取。
	return u != nil && u.Scheme == "https" &&
		strings.EqualFold(u.Host, "api.example.com")
}

func GetWithToken(ctx context.Context, start, token string) (*http.Response, error) {
	client := &http.Client{
		// 让上层拿到 3xx,由本函数完成逐跳校验和认证注入。
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			return http.ErrUseLastResponse
		},
	}
	current, err := url.Parse(start)
	if err != nil || !trustedAPI(current) {
		return nil, ErrRedirectTarget
	}

	for hop := 0; hop = 400 {
			return resp, nil
		}

		next, err := resp.Location()
		resp.Body.Close()
		if err != nil || !trustedAPI(next) {
			return nil, fmt.Errorf("%w: %v", ErrRedirectTarget, err)
		}
		current = next
	}
	return nil, errors.New("too many redirects")
}

这段写法的关键不是“总是带 Token”,而是把发送认证头的条件收紧为“目标 URL 已经过 allowlist 判断”。如果实际业务是从 auth.example.com 跳向 api.example.com,就把两个精确 Origin 都纳入协议设计,并明确每一跳能接收哪一种凭证。

状态码、响应体和常见排查误区

现象更可能的原因处理建议
跨主机后 401Authorization 被默认敏感头策略移除检查 Location 主机,使用精确 allowlist 或手动跟随
同主机仍未到达业务处理CheckRedirect 返回了错误,或超过默认重定向次数区分 url.Error 与最终 HTTP 状态码,记录主机而不记录令牌
返回 3xx 后程序挂起或连接复用异常使用 ErrUseLastResponse 后没有关闭响应体读取需要的头和状态后显式调用 resp.Body.Close()
POST 跳转后变成 GET301/302/303 的方法处理规则需要保留方法和请求体时确认 307/308 与 GetBody 条件

排查时可以按三件事顺序看:第一,打印原始 URL 与下一跳 URL 的主机、协议和端口;第二,在 CheckRedirect 中只记录认证头是否存在;第三,确认是否主动返回了 ErrUseLastResponse 或自定义错误。不要通过打印 Bearer Token 来证明 Header 是否转发,也不要只把“最终响应是 401”解释成服务端令牌失效。

相关问题

Go 会不会把 Authorization 转发到子域名?

默认策略可能会转发到原主机的子域名,但这不等于你的业务安全边界。对于敏感凭证,建议在 CheckRedirect 中改用精确主机、端口和 HTTPS 条件。

怎样只禁止跨主机跳转?

在 CheckRedirect 里用 via[0].URL.Host 与 req.URL.Host 做精确比较,发现不同就返回自定义错误;这样不会把令牌重新注入到未知目标。

返回 ErrUseLastResponse 后还要关闭 Body 吗?

要。这个特殊返回值会把最近的 3xx 响应和未关闭的 Body 交给调用方,读取完需要的响应头或内容后应显式关闭。

跨主机跳转一定要手动处理吗?

不一定。如果跨主机只是错误配置,失败关闭更安全;如果它是明确的协议流程,就应把目标主机、协议、凭证类型和最大跳数写进 allowlist,再手动构造下一跳请求。

总之,认证头在 HTTP 重定向后消失,多数时候是 Go 在替你守住主机边界。先让默认保护工作,再用精确的 CheckRedirect 或手动跟随策略表达真实信任关系,通常比“无论跳到哪里都补回 Authorization”更可靠。

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