北川广海の梦

北川广海の梦

Go实现反向代理,并自动处理重定向

97
2023-06-28
Go实现反向代理,并自动处理重定向

反向代理

说起反向代理,第一印象估计都是超高人气的Nginx,其凭借强大的性能,吸引了不少使用者。

为什么需要反向代理?

  • 隐藏服务器细节:在你的反向代理机器背后,就是你真正的业务服务器。它可能是由上千台机器组成的庞大集群,也可能是一个便宜好用的云服务器。外部请求想要访问你的服务器,必须通过反向代理。并且假如你的服务器内部有重定向细节,反向代理服务器可以很好的屏蔽,让客户端完全无感知
  • 负载均衡:nginx就是一个很好的负载均衡器,能很好的平衡服务器资源
  • 统一SSL:不必让内部服务器执行这毫无必要的SSL过程,将资源解放出来专注业务服务
  • 静态资源缓存:提供网页、文件的静态服务能力,并能提供压缩等节省服务器资源的手段

Go实现反向代理,并自动重定向

Go语言作为一个工具语言,其标准库就为我们提供了大量好用的工具。

今天的主角就是httputil.ReverseProxy 通过它,我们就可以立即获得一个基本的反向代理服务器,实现对请求的转发。

同时,它通过拦截器的设计,为我们提供了可扩展的能力,我们将基于它实现: “当目标服务器返回302响应时,自动转发请求到新的目标服务器上,完全屏蔽客户端的感知”。

首先,我们通过httputil.NewSingleHostReverseProxy创建一个反向代理,它接受一个url参数,这个参数就是原始目标服务器的地址。创建完毕后,所有被这个proxy serve的请求,都会转发到原始目标服务器了

// create a ReverseProxy, it forward all request to url
proxy = httputil.NewSingleHostReverseProxy(url)

接下来,我们设置ModifyResponse,它的作用是:对目标服务器的响应判断,如果需要302,那么返回一个我们自定义的重定向错误

type RedirectErr struct {
	location *urlpkg.URL
}

func (e *RedirectErr) Error() string {
	return "the request should redirect"
}

proxy.ModifyResponse = func(response *http.Response) error {
			if response.StatusCode > 300 && response.StatusCode < 400 {
				location, err := response.Location()
				if err != nil {
					return err
				}
				return &RedirectErr{location: location}
			}
			return nil
		}

接着我们需要对上一步返回的错误进行处理:如果发现它是重定向错误,那么我们修改请求头,添加一个字段,包含重定向目标服务器的地址,然后重新serve

proxy.ErrorHandler = func(writer http.ResponseWriter, request *http.Request, err error) {
			var redirect *RedirectErr
			if redirect, ok = err.(*RedirectErr); !ok {
				return
			}
			request.Header.Set("redirect", redirect.location.String())
			proxy.ServeHTTP(writer, request)
		}

我们通过Rewrite函数来对转发处理,这个函数如果检测到请求存在redirect字段,就会修改目标地址为redirect的值。否则还是转发到httputil.NewSingleHostReverseProxy(url)的参数的URL。这里的rewriteRequestURL函数就是从标准库复制出来的,作用就是修改request的url,以遍能将请求转发到target上。注意,这里我们通过存放在context里的body,进行了rewind,这是因为 request.body只能读取一次,第二次转发的时候,它将无法读取,所以需要还原。

porxy.Director = nil
proxy.Rewrite = func(request *httputil.ProxyRequest) {
			if redirect := request.In.Header.Get("redirect"); redirect != "" {
				request.In.Header.Del("redirect")
				u, _ := urlpkg.Parse(redirect)

				// rewind body
				if body := request.In.Context().Value(bodyKey); body != nil {
					if bodyBytes, ok := body.([]byte); ok {
						request.Out.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
					}
				}
				request.SetURL(u)
				request.Out.Host = u.Host
				return
			}
			request.SetURL(url)
			request.Out.Host = url.Host
		}

我们在 serve 之前,先把 body存起来,

// The body can only be read once, so we store it to make the body available on the second forwarding
	if req.Body != nil {
		bodyBytes, _ := io.ReadAll(req.Body)
		req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
		req = req.WithContext(context.WithValue(req.Context(), bodyKey, bodyBytes))
	}

	p.ReverseProxy.ServeHTTP(rw, req)