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

Go http.Client CheckRedirect 处理跨域跳转的配置方法

来源:17golang原创

时间:2026-11-13 09:30:41 445浏览 收藏

Go 的 http.Client 默认会处理常见的 3xx 重定向,但遇到登录跳转、短链或跨主机地址时,直接放行并不总是合适。这里的“跨域”特指重定向目标的主机或 scheme 发生变化,不是浏览器里的 CORS。实用做法是给 Client 配置 CheckRedirect:从 req.URL 判断当前目标,利用 via 了解跳转历史,再决定继续、保留这次 3xx 响应,还是返回策略错误。

官方地址:https://pkg.go.dev/net/http#Client.CheckRedirect

要点速览
  • 只想阻止自动跟随并读取 Location 时,返回 http.ErrUseLastResponse,得到最后一次响应且不把它当成普通错误。
  • 不可信目标应在 CheckRedirect 中比较原始 host、目标 host 和白名单;拒绝策略可返回自定义错误。
  • 普通错误会被 http.Client.Do 包装成 *url.Error,策略拒绝和网络失败要在调用方分开记录。

先区分三种重定向结果

CheckRedirect 的回调参数是即将发出的 req 和已经走过的请求切片 via。返回 nil 表示允许 Client 继续;返回普通错误表示停止并把错误交给调用方;返回特殊值 http.ErrUseLastResponse 时,Client 返回最近一次响应,响应体保持打开,适合拿到原始 3xx 状态和 Location。

Go http.Client CheckRedirect、req.URL、via 与自动跟随、ErrUseLastResponse、url.Error 三种结果的静态关系说明图
图1:CheckRedirect 返回策略说明图,展示请求策略与结果对象的关系,不是运行截图。
回调返回值Client.Do 的结果适合场景
nil继续跟随重定向目标在可信范围内
http.ErrUseLastResponse返回最近一次 3xx 响应,Body 不自动关闭读取 Location 或保留原始状态
其他 error停止并返回 *url.Error拒绝不可信主机或超过业务限制

用目标 URL 判断是否允许跨主机跳转

不要只判断“发生了重定向”就一律拒绝。更稳妥的边界是记录初始主机,只允许同主机,或允许一个明确的目标白名单。req.URL 是即将访问的目标,via 的第一个元素通常能帮助你回看原始请求。下面的示例只展示策略判断;生产代码还应根据业务需要处理端口、scheme 和代理场景。

Go CheckRedirect 对原始 host、目标 host、Location 和白名单进行同主机与跨主机边界判断的说明图
图2:跨主机重定向边界说明图,展示白名单判断与拒绝分支,不是运行证据。
package main

import (
    "fmt"
    "net/http"
    "net/url"
)

func newRedirectClient() *http.Client {
    // 白名单只放业务确实需要跳转到的主机,避免把任意 Location 当成可信目标。
    allowedHosts := map[string]bool{
        "login.example.com": true,
        "api.example.com":   true,
    }

    return &http.Client{
        CheckRedirect: func(req *http.Request, via []*http.Request) error {
            if len(via) == 0 {
                return nil // 没有历史请求时,无法比较原始目标,先交给默认策略。
            }
            originalHost := via[0].URL.Hostname()
            targetHost := req.URL.Hostname()
            if targetHost == originalHost || allowedHosts[targetHost] {
                return nil // 同主机或白名单目标可以继续跟随。
            }
            return fmt.Errorf("拒绝跨主机重定向: %s -> %s", originalHost, targetHost)
        },
    }
}

func doRequest(client *http.Client, rawURL string) error {
    resp, err := client.Get(rawURL)
    if err != nil {
        // Client 会把 CheckRedirect 的普通错误包装成 *url.Error,保留原因链便于区分策略拒绝。
        var urlErr *url.Error
        if ok := errorAs(err, &urlErr); ok {
            return fmt.Errorf("请求被重定向策略终止: %w", urlErr)
        }
        return err
    }
    defer resp.Body.Close() // 成功拿到最终响应后,调用方负责关闭响应体。
    return nil
}

// errorAs 只为让示例保持短小;真实代码可直接使用 errors.As。
func errorAs(err error, target **url.Error) bool {
    for err != nil {
        if value, ok := err.(*url.Error); ok {
            *target = value
            return true
        }
        type unwrapper interface{ Unwrap() error }
        next, ok := err.(unwrapper)
        if !ok {
            return false
        }
        err = next.Unwrap()
    }
    return false
}

示例里的 errorAs 是为了展示错误链判断,真实项目建议使用标准库 errors.As。另外,Go 客户端对不可信目标的敏感请求头有自己的转发规则;这和是否允许业务跳转是两层策略,不应只靠“跨域”这个词做判断。

需要读取 3xx 时保留最后一次响应

如果需求是查看跳转地址,而不是把拒绝当异常,可以返回 http.ErrUseLastResponse。官方语义是把最近一次响应交给调用方,同时不替你关闭它的 Body,因此读取或丢弃完内容后必须关闭。

client := &http.Client{
    CheckRedirect: func(req *http.Request, via []*http.Request) error {
        // 返回特殊错误会保留最近一次 3xx 响应,而不是生成普通 url.Error。
        if len(via) > 0 && req.URL.Hostname() != via[0].URL.Hostname() {
            return http.ErrUseLastResponse
        }
        return nil // 同主机跳转继续交给 Client 处理。
    },
}

resp, err := client.Get("https://example.com/start")
if err != nil {
    return err // 网络错误或其他重定向策略错误仍从这里返回。
}
defer resp.Body.Close() // ErrUseLastResponse 下也必须负责关闭 Body。
fmt.Println(resp.StatusCode, resp.Header.Get("Location")) // 读取原始 3xx 和目标地址。

这个分支适合做重定向分析、记录 Location 或让上层决定下一步。若返回自定义错误,调用方通常拿不到一个可继续读取的最近响应,记录日志时应保留 url.Error 的 Err 链。

按任务选择配置边界

实际排查时可以先按下面的清单选策略,再写回调,避免把所有 3xx 都当成同一种情况:

任务判断重点回调策略
正常登录跳转目标 host 在业务白名单返回 nil
记录短链最终去向需要保留当前 Location返回 ErrUseLastResponse
禁止跳到外部站点目标 host 不在允许集合返回带原因的普通 error
限制循环跳转检查 len(via) 和业务上限超限返回普通 error

常见问题

Go 客户端的跨域跳转等同于浏览器 CORS 吗?

不是。这里讨论的是 HTTP 客户端是否跟随新的 host 或 scheme;CORS 是浏览器对脚本发起跨源请求的访问控制模型。

为什么返回 ErrUseLastResponse 后没有 error?

这是标准库专门保留最后一次响应的分支,调用方应检查响应状态和 Location,并在读取后关闭 Body。

via 为空时能不能直接取 via[0]?

不能。回调可能在没有历史请求的情况下被调用,访问前先判断长度,否则会触发切片越界。

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