测试基础 简单的反向代理来解决动态修改转发地址和请求头

JoyMao · 2022年09月30日 · 最后由 Willen 回复于 2022年10月19日 · 12893 次阅读
本帖已被设为精华帖!

半年前,为了解决测试环境多套应用灵活切换来接受外部系统回调问题,利用 nginx 的 dynamic_upstream 插件 + 封装 httpAPI 来随时更新 upstream 的方式解决(https://testerhome.com/topics/32676

但几月前,多套测试环境切换已经改成使用公共代理(nohosts)+ 特殊请求头(标记去请求哪套环境或开发的后端服务)的方式了,原方式已经失效。

为了不修改原回调转发器的功能,给每个测试环境增加了 1 个 nginx,将每个 nginx 的 ip 提供给回调转发器,并且每个 niginx 再二次转发时设置上对应的特殊请求头。
一开始的临时方案如下:

但是这个临时方案增加系统的复杂度,排查错误更加麻烦,需要检查更多的节点。

为了简化结构,google 找了一圈工具,没找到合适、轻量且满足以下要求的工具:

  • 支持反向代理服务
  • 可提供 http api 来动态的修改转发地址及设置请求头

想了想,还是自写工具来代替 nginx,因为原封装的 http api 服务是 go 写的,只要在其里面另 go 一个协程启动反向代理服务("net/http/httputil"下的 NewSingleHostReverseProxy)出来即可。
利用 http api 修改全局的转发配置,反向代理每次按照当前配置转发和设置请求头即可满足。

http-api 服务是主程(使用的 iris 框架只是图方便,后续加上权限控制也容易),不赘述。

反向代理服务简单说明

  1. 转发初始配置中,一个服务 target 对应多个 route,请求根据 route 中 path 正则匹配找到对应转发 target,然后覆写 path 和设置请求头
ProxyServ: ':82'
Confs:
  - Name: pay
    Target: http://127.0.0.1:80
    Routes:
      - Name: stripe-xxx
        Host: pay.xxx.com
        Path: '(/xxx-com/callback)/(stripexxx)'
        RePath: '{$1}/{$2}'
        ReqHeaders:
          - Name: Environment-Label
            Value: trunk
      - Name: payoneer
        Host: pay.xxx.com
        Path: '(/xxx-com/callback)/payoneer'
        RePath: '{$1}/payoneer'
        ReqHeaders:
          - Name: Environment-Label
            Value: trunk
  - Name: dd_social_pay
    Target: http://127.0.0.1:80
    Routes:
     - Name: stripe-xxx-dd
       Host: pay.xxx.com
       Path: '/callback/stripexxx/connect'
       RePath: '/callback/connect'
       ReqHeaders:
          - Name: Environment-Label
            Value: trunk

对应 go 中 struct:

// 请求头设置
type ReqHeader struct {
    Name  string `yaml:"Name"`
    Value string `yaml:"Value"`
}
// 路由设置
type Route struct {
    Name       string       `yaml:"Name"`
    Host       string       `yaml:"Host"`
    Path       string       `yaml:"Path"`
    RePath     string       `yaml:"RePath"`
    ReqHeaders []*ReqHeader `yaml:"ReqHeaders"`
}
// 单个反向设置
type SigleReverse struct {
    Name   string   `yaml:"Name"`
    Target string   `yaml:"Target"`
    Routes []*Route `yaml:"Routes"`
}
// 简单反向代理设置
type ReverseConf struct {
    ProxyServ string          `yaml:"ProxyServ"`
    Confs     []*SigleReverse `yaml:"Confs"`
}
  1. 因为要实时获取转发配置并进行对应转发处理,需要修改原版中的 proxy.Director,并将原版中私有方法 singleJoiningSlash、joinURLPath 拷贝一份出来使用:
proxy.Director = func(req *http.Request) {
//sr是全局配置中一个服务配置(SigleReverse),Target为其转发目标
                    target, err := url.Parse(strings.TrimSpace(sr.Target))
                    if err != nil {
                        log.Printf("转发的URL有误:%s", sr.Target)
                    }
                    targetQuery := target.RawQuery
                    req.URL.Scheme = target.Scheme
                    req.URL.Host = target.Host
                    req.URL.Path, req.URL.RawPath = joinURLPath(target, req.URL)
                    if targetQuery == "" || req.URL.RawQuery == "" {
                        req.URL.RawQuery = targetQuery + req.URL.RawQuery
                    } else {
                        req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
                    }
                    if _, ok := req.Header["User-Agent"]; !ok {
                        req.Header.Set("User-Agent", "")
                    }
                    xff := strings.TrimSpace(req.Header.Get("X-Forwarded-For"))
                    if xff != "" {
                        xff = xff + "," + strings.Split(req.RemoteAddr, ":")[0]
                    } else {
                        xff = strings.Split(req.RemoteAddr, ":")[0]
                    }
                    req.Header.Set("X-Forwarded-For", xff)
                    for _, srh := range rt.ReqHeaders {
                        req.Header.Set(srh.Name, srh.Value)
                    }
                    if strings.TrimSpace(rt.Host) != "" {
                        req.Host = rt.Host
                    }
                    req.URL.Path = newPath
                    log.Printf("url:%s, host:%s, xff:%s\n", req.URL.String(), req.Host, req.Header.Get("X-Forwarded-For"))
                }
                proxy.ServeHTTP(w, r)
  1. 每个请求,通过遍历转发配置,匹配 path 的正则来找到对应 target 和进行 route 中修改 path 和请求头即可完成功能。
  2. 这样简化后的回调切换结构如下
  3. 再利用 vue 画个前端页面就完工了

github:
https://github.com/mao303mao/go_dynamic_reverse_proxy

共收到 6 条回复 时间 点赞

为楼主的这份思考和实践分享点赞,感谢楼主末尾还分享了这个轻量级反向代理工具。

多套测试环境切换已经改成使用公共代理(nohosts)+ 特殊请求头(标记去请求哪套环境或开发的后端服务)的方式了

好奇问下,为何不能直接沿用这套机制呢?我理解这套机制里,应该有一些手段记录某个主机的请求来了后,是走哪个环境的。这套机制应该也能适用于主机地址相对固定的第三方回调?

另外,这个统一对外,并基于配置转发给内部其他服务的反向代理,在很多的架构中会称为网关(gateway),或许楼主把搜索关键字改为 gateway ,会找到一些合适的工具。

陈恒捷 回复

不沿用的原因是 nginx 的那个动态插件是不支持通过 http 接口动态设置请求头的,所以有临时方案在每套环境之前放 1 个 nginx 来设置对应环境的特殊请求头。

确实找过一些 “Gateway” 类的工具,但大都比较 “大而全”,放在这里有些小题大做。而且不少工具不支持通过 api 动态修改配置,需要改造,就放弃了。

这里只是一个小工具,功能精简且好移植(go 语言)

陈恒捷 将本帖设为了精华贴 10月06日 03:05

加个精,方便更多同学参考借鉴,以及用上楼主提供的工具快速解决同类问题。

我是用 koa+http-proxy-middleware 来实现的,异常方便和轻量。

收藏了赞👍🏻

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册