You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何修改Golang反向代理的路径/路由以注入服务前缀

解决方案:修复路径注入与重定向处理

首先修正你代码里的两个明显bug:

  1. req.url.path 应为 req.URL.Path(Go语言中结构体字段首字母大写才对外可见)
  2. TrimPrefix 的参数应该是动态的服务名,而非固定字符串 "/serviceName",改为 "/"+serviceName

接下来针对核心需求,分两步解决路径注入问题:

一、处理重定向响应头

当remote1返回重定向(比如301/302状态码)时,Location 头中的路径是不带 /service1 前缀的,必须手动添加,否则浏览器会直接跳转到错误的路径。

在获取到 serverResponse 后添加以下逻辑:

import "net/url"
import "strings"

// 处理重定向Location头
if location := serverResponse.Header.Get("Location"); location != "" {
    u, err := url.Parse(location)
    if err == nil {
        // 如果是相对路径,直接添加服务前缀;如果是remote1的绝对路径,替换路径部分
        if u.IsAbs() && u.Host == hostName {
            u.Path = "/" + serviceName + u.Path
            serverResponse.Header.Set("Location", u.String())
        } else if !u.IsAbs() {
            serverResponse.Header.Set("Location", "/"+serviceName+location)
        }
    }
}

二、修改响应体中的路径

页面内的链接、静态资源(CSS/JS/图片)路径都是相对路径,需要批量修改为带 /service1 前缀的路径。用HTML解析库(如goquery)比正则更可靠,避免误匹配。

步骤1:安装goquery

go get github.com/PuerkitoBio/goquery

步骤2:修改响应体处理逻辑

替换原代码中 io.Copy(res, serverResponse.Body) 的部分:

import "github.com/PuerkitoBio/goquery"
import "strconv"
import "strings"

// 读取响应体到缓冲区
bodyBytes, err := io.ReadAll(serverResponse.Body)
serverResponse.Body.Close()
if err != nil {
    res.WriteHeader(http.StatusInternalServerError)
    fmt.Fprint(res, "Failed to read upstream response")
    return
}

bodyStr := string(bodyBytes)
// 仅处理HTML类型的响应
if strings.Contains(serverResponse.Header.Get("Content-Type"), "text/html") {
    doc, err := goquery.NewDocumentFromReader(strings.NewReader(bodyStr))
    if err == nil {
        // 修改所有href属性(链接)
        doc.Find("[href]").Each(func(i int, s *goquery.Selection) {
            href, exists := s.Attr("href")
            if exists && !strings.HasPrefix(href, "http") && !strings.HasPrefix(href, "#") && !strings.HasPrefix(href, "data:") {
                s.SetAttr("href", "/"+serviceName+href)
            }
        })
        // 修改所有src属性(静态资源)
        doc.Find("[src]").Each(func(i int, s *goquery.Selection) {
            src, exists := s.Attr("src")
            if exists && !strings.HasPrefix(src, "http") && !strings.HasPrefix(src, "data:") {
                s.SetAttr("src", "/"+serviceName+src)
            }
        })
        // 重新生成HTML内容
        bodyStr, _ = doc.Html()
    }
}

// 更新Content-Length头(内容修改后长度变化)
res.Header().Set("Content-Length", strconv.Itoa(len(bodyStr)))
// 写入修改后的响应
res.Write([]byte(bodyStr))

三、不推荐iframe方案的原因

用iframe虽然能快速实现路径隔离,但会带来诸多问题:

  • 跨域限制导致部分功能失效(如Cookie共享、页面跳转)
  • 浏览器前进/后退按钮无法正常工作
  • SEO不友好,搜索引擎无法正确抓取内容
  • 嵌套层级带来的样式、布局兼容性问题

完整修正后的proxy函数示例

func proxy(serviceName string) func(res http.ResponseWriter, req *http.Request) {
    return func(res http.ResponseWriter, req *http.Request) {
        cnn, err := dialer.Dial("tcp", ":1250") // 按你的架构需求保留
        if err != nil {
            res.WriteHeader(http.StatusInternalServerError)
            return
        }
        defer cnn.Close() // 记得关闭连接,避免资源泄漏

        transport := &http.Transport{
            Dial: func(network, addr string) (net.Conn, error) {
                return cnn, nil
            },
        }
        client := &http.Client{
            Transport: transport,
        }

        hostName := hostNameMap[serviceName]
        req.URL.Host = req.Host
        req.URL.Scheme = originSvr.Scheme
        req.RequestURI = ""
        // 修正:动态移除服务名前缀
        req.URL.Path = strings.TrimPrefix(req.URL.Path, "/"+serviceName)

        serverResponse, err := client.Do(req)
        if err != nil {
            res.WriteHeader(http.StatusInternalServerError)
            fmt.Fprint(res, err.Error())
            return
        }
        defer serverResponse.Body.Close()

        // 处理重定向Location头
        if location := serverResponse.Header.Get("Location"); location != "" {
            u, err := url.Parse(location)
            if err == nil {
                if u.IsAbs() && u.Host == hostName {
                    u.Path = "/" + serviceName + u.Path
                    serverResponse.Header.Set("Location", u.String())
                } else if !u.IsAbs() {
                    serverResponse.Header.Set("Location", "/"+serviceName+location)
                }
            }
        }

        // 复制响应头到客户端
        for k, v := range serverResponse.Header {
            res.Header()[k] = v
        }
        res.WriteHeader(serverResponse.StatusCode)

        // 读取并修改响应体
        bodyBytes, err := io.ReadAll(serverResponse.Body)
        if err != nil {
            fmt.Fprint(res, "Failed to read upstream response")
            return
        }

        bodyStr := string(bodyBytes)
        if strings.Contains(serverResponse.Header.Get("Content-Type"), "text/html") {
            doc, err := goquery.NewDocumentFromReader(strings.NewReader(bodyStr))
            if err == nil {
                doc.Find("[href]").Each(func(i int, s *goquery.Selection) {
                    href, exists := s.Attr("href")
                    if exists && !strings.HasPrefix(href, "http") && !strings.HasPrefix(href, "#") && !strings.HasPrefix(href, "data:") {
                        s.SetAttr("href", "/"+serviceName+href)
                    }
                })
                doc.Find("[src]").Each(func(i int, s *goquery.Selection) {
                    src, exists := s.Attr("src")
                    if exists && !strings.HasPrefix(src, "http") && !strings.HasPrefix(src, "data:") {
                        s.SetAttr("src", "/"+serviceName+src)
                    }
                })
                bodyStr, _ = doc.Html()
            }
        }

        res.Header().Set("Content-Length", strconv.Itoa(len(bodyStr)))
        res.Write([]byte(bodyStr))
    }
}

内容的提问来源于stack exchange,提问作者solonly

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 09:15:03