最近在一个 Go Web 项目工作,接到一个需求,对接口进行加密转发,也就是将原本的所有接口的请求路径、请求参数、请求和 Header 和 Body 都进行加密,通过请求 body 统一提交给一个固定的接口,然后在服务端请求解开数据进行逻辑调用,最后将相应结果再加密返回。以增加追踪和分析接口的难度。项目使用的 Web 开发框架是 Kratos。
在不改动原有接口的情况下,实现这类需求一般有两种方法,一种是通过拦截器或者中间件来修改请求信息,第二种是在接口中根据请求数据进行请求转发。下面分别介绍两种方法的实现方式。
第一种,中间件方案。
在 Kratos 中,类似拦截器、过滤器的 Web 组件,被称为中间件或 Middleware,处理请求的逻辑叫 Handler。逻辑关系如下:
vbscript
┌───────────────────┐
│MIDDLEWARE 1 │
│ ┌────────────────┐│
│ │MIDDLEWARE 2 ││
│ │ ┌─────────────┐││
│ │ │MIDDLEWARE 3 │││
│ │ │ ┌─────────┐ │││
REQUEST │ │ │ │ YOUR │ │││ RESPONSE
──────┼─┼─┼─▷ HANDLER ○─┼┼┼───▷
│ │ │ └─────────┘ │││
│ │ └─────────────┘││
│ └────────────────┘│
└───────────────────┘
Kratos 中有很多内置的中间件,这里我们要实现的功能,则需要自定义中间件。我们需要在中间件中完成以下几件事儿:
- 对请求的 body 信息进行解密,并解析成 JSON 对象,方便获取其中的信息。
- 根据解析出的 JSON 对象,获取要请求的接口的路径、请求参数、请求头(其中包含认证信息,如 jwt)、请求体。
- 根据解析出的以上信息,对当前的请求对象进行修改
- 进入下一个中间件或者请求处理的逻辑
在 Kratos 中自定义中间件需要一个返回 middleware.Middleware
的函数,它的类型定义如下:
go
type Middleware func(Handler) Handler
其中的 Handler 的类型定义如下:
go
type Handler func(ctx context.Context, req any) (any, error)
也就是通过参数传入一个 Handler,增加一些中间件的处理逻辑,然后返回一个新的 Handler。要实现需求,需要先定义一个结构体,用来约定前端请求时组织请求信息的结构,比如下面这样:
go
type InnerRequest struct {
Path string `json:"path"`
Method string `json:"method"`
Headers http.Header `json:"headers"`
Body string `json:"body"` // base64 encoded body
}
前端将真实的请求意图,封装成 InnerRequest 结构并加密,通过请求 body 给到服务端,在中间件中,服务端通过解密和解析,得到这些信息,根据这些信息对请求(Request)进行修改。
首先,在中间件中从 Context 中获取到请求对象:
go
tr, ok := transport.FromServerContext(ctx)
if !ok {
// For non-HTTP transports, skip this middleware.
return handler(ctx, req)
}
httpReq, ok := tr.(kratoshttp.Transporter)
if !ok {
return handler(ctx, req)
}
然后对原始的请求和上下文进行修改,并调用下一层中间件调用:
go
httpReq.Request().Method = innerReq.Method
parsedURL, err := url.Parse(innerReq.Path)
if err != nil {
return nil, kerrors.BadRequest("INVALID_URL", "failed to parse inner request URL")
}
httpReq.Request().URL.Path = parsedURL.Path
httpReq.Request().URL.RawQuery = parsedURL.RawQuery // URL 参数
httpReq.Request().Body = io.NopCloser(bytes.NewBufferString(innerReq.Body))
// Also copy the headers from the inner request to the actual request
for key, values := range innerReq.Headers {
httpReq.Request().Header[key] = values
}
其中有一步比较重要,就是 URL 中的请求参数这里需要专门处理一下,否则根据带参数的 URL 查找请求处理逻辑的话会报 404。
最后就是把中间件配置到 HTTPServer 中就可以了。
第二种,接口转发方案
这种方案是配置一个单独的接口,用来接受请求,并在处理时构建新的请求,在内部调用原有接口的请求逻辑。
这类特殊的接口,可以直接根据路径分配一个自定义的 Handler:
go
entrypointHandler := &EntryPointHandler{
kratosServer: srv,
apiConf: apiConf,
}
srv.Handle("/api/entrypoint", entrypointHandler)
EntryPointHandler 的定义如下:
go
type EntryPointHandler struct {
kratosServer stdhttp.Handler // The Kratos server itself
apiConf *conf.Api
}
其中,kratosServer 是 Kratos 本身的 server,便于处理完请求之后构建新的请求再交给 Kratos 处理后续的逻辑,apiConf 是一些配置信息,如加密的密钥等。EntryPointHandler 需要实现一个接口来处理请求:
go
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
这个接口中可以操作 request 和 responseWriter,这样,就能够对请求进行各种自定义处理,到此,剩下的主要逻辑其实跟中间件方案的差不多了,只是这里不是要修改原请求,而是构建新的请求对象,并交给 Kratos 处理。以下是关键部分的代码(删除了错误处理和不重要的逻辑):
go
func (h *EntryPointHandler) ServeHTTP(w stdhttp.ResponseWriter, r *stdhttp.Request) {
// 1. 获取请求内容
body, err := io.ReadAll(r.Body)
// 2. 解析请求内容 JSON
// code
// 3. 解密
// code
// 4. 解析解密后的请求内容
var innerReq pkgMiddleware.InnerRequest
// code
// 5. 封装真实的请求信息
newReq := r.Clone(r.Context())
// 请求 URL 和 URL 参数
parsedURL, err := url.Parse(innerReq.Path)
newReq.URL.Path = parsedURL.Path
newReq.URL.RawQuery = parsedURL.RawQuery
// 请求方法
newReq.Method = innerReq.Method
// 请求体
newReq.Body = io.NopCloser(bytes.NewReader([]byte(innerReq.Body)))
newReq.ContentLength = int64(len(innerReq.Body))
// 请求头
for key, values := range innerReq.Headers {
if values != nil {
newReq.Header[key] = values
}
}
// 6. Context
newReq = newReq.WithContext(ctx)
// 7. 转发请求
h.kratosServer.ServeHTTP(w, newReq)
}
最后一步就是把 responseWriter 和新构建的请求 newReq 交给 Kratos 处理了。
返回信息的处理
除了处理请求信息,返回的 response 也需要加密处理,这部分交给 Kratos 的 EncoderResponse
来处理,EncoderResponse 的专门用来处理 Kratos 的返回信息。在 EncoderResponse 中只需要将处理结果对象序列化成 JSON 然后再进行加密,写入 responseWriter 中就可以了,这部分比较简单就补贴代码了。