用Go语言写HTTP中间件

开发 前端
在web开发过程中,中间件一般是指应用程序中封装原始信息,添加额外功能的组件。不知道为什么,中间件通常是一种不太受欢迎的概念。但我认为它棒极了

web开发过程中,中间件一般是指应用程序中封装原始信息,添加额外功能的组件。不知道为什么,中间件通常是一种不太受欢迎的概念。但我认为它棒极了。

其一,一个好的中间件拥有单一的功能,可插拔并且是自我约束的。这就意味着你可以在接口的层次上把它放到应用中,并能很好的工作。中间件并不影响你 的代码风格,它也不是一个框架,仅仅是你处理请求流程中额外一层罢了。根本不需要重写代码:如果你想用一个中间件,就把它加上应用中;如果你改变主意了, 去掉就好了。就这么简单。

来看看GoHTTP中间件非常流行,标准库中也是这样。或许咋看上去并不明显,net/http包中的函数,如StripPrefix 和TimeoutHandler 正是我们上面定义的中间件:封装处理过程并在处理输入或输出时增加额外的动作。

我最近的Gonosurf 也是一个中间件。我从一开始就有意的这样设计。大多数情况下,你根本不必在应用层关心CSRF检查。nosurf,和其他中间件一样,非常独立,可以和实现标准库net/http接口的工具配合使用。

你也可以使用中间件做这些:

  • 通过隐藏长度缓解BREACH攻击
  • 频率限制
  • 屏蔽恶意自动程序
  • 提供调试信息
  • 添加HSTS, X-Frame-Options头
  • 从异常中优雅恢复
  • 以及其他等等。

写一个简单的中间件

第一个例子中,我写了一个中间件,只允许用户从特定的域(在HTTPHost头中有域信息)来访问服务器。这样的中间件可以保护应用程序不受“主机欺骗攻击

定义类型

为了方便,让我们为这个中间件定义一种类型,叫做SingleHost

  1. type SingleHost struct { 
  2.   
  3.     handler     http.Handler 
  4.   
  5.     allowedHost string 
  6.   

只包含两个字段:

  • 封装的Handler。如果是有效的Host访问,我们就调用这个Handler
  • 允许的主机值。

由于我们把字段名小写了,使得该字段只对我们自己的包可见。我们还应该写一个初始化函数。

  1. func NewSingleHost(handler http.Handler, allowedHost string) *SingleHost { 
  2.   
  3.     return &SingleHost{handler: handler, allowedHost: allowedHost} 
  4.   

处理请求

现在才是实际的逻辑。为了实现http.Handler,我们的类型秩序实现一个方法:

  1. type Handler interface { 
  2.   
  3.         ServeHTTP(ResponseWriter, *Request) 
  4.   

这就是我们实现的方法:

  1. func (s *SingleHost) ServeHTTP(w http.ResponseWriter, r *http.Request) { 
  2.   
  3.     host :r.Host 
  4.   
  5.     if host == s.allowedHost { 
  6.   
  7.         s.handler.ServeHTTP(w, r) 
  8.   
  9.     } else { 
  10.   
  11.         w.WriteHeader(403) 
  12.   
  13.     } 
  14.   

ServeHTTP 函数仅仅检查请求中的Host头:

  • 如果Host头匹配初始化函数设置的allowedHost ,就调用封装handlerServeHTTP方法。
  • 如果Host头不匹配,就返回403状态码(禁止访问)。

在后一种情况中,封装handlerServeHTTP方法根本就不会被调用。因此封装的handler根本不会有任何输出,实际上它根本就不知道有这样一个请求到来。

现在我们已经完成了自己的中间件,来把它放到应用中。这次我们不把Handler直接放到net/http服务中,而是先把Handler封装到中间件中。

  1. singleHosted = NewSingleHost(myHandler, "example.com") 
  2.   
  3. http.ListenAndServe(":8080", singleHosted) 

另外一种方法

我们刚才写的中间件实在是太简单了,只有仅仅15行代码。为了写这样的中间件,引入了一个不太通用的方法。由于Go支持函数第一型和闭包,并且拥有简洁的http.HandlerFunc包装器,我们可以将其实现为一个简单的函数,而不是写一个单独的类型。下面是基于函数的中间件版本。

  1. func SingleHost(handler http.Handler, allowedHost string) http.Handler { 
  2.   
  3.     ourFunc :func(w http.ResponseWriter, r *http.Request) { 
  4.   
  5.         host :r.Host 
  6.   
  7.         if host == allowedHost { 
  8.   
  9.             handler.ServeHTTP(w, r) 
  10.   
  11.         } else { 
  12.   
  13.             w.WriteHeader(403) 
  14.   
  15.         } 
  16.   
  17.     } 
  18.   
  19.     return http.HandlerFunc(ourFunc) 
  20.   

#p#

这里我们声明了一个叫做SingleHost的简单函数,接受一个Handler和允许的主机名。在函数内部,我们创建了一个类似之前版本ServeHTTP的函数。这个内部函数其实是一个闭包,所以它可以从SingleHost外部访问。最终,我们通过HandlerFunc把这个函数用作http.Handler

使用Handler还是定义一个http.Handler类型完全取决于你。对简单的情况而已,一个函数就足够了。但是随着中间件功能的复杂,你应该考虑定义自己的数据结构,把逻辑独立到多个方法中。

实际上,标准库这两种方法都用了。StripPrefix 是一个返回HandlerFunc的函数。虽然TimeoutHandler也是一个函数,但它返回了处理请求的自定义的类型。

更复杂的情况

我们的SingleHost中间件非常简单:先检查请求的一个属性,然后要么什么也不管,把请求直接传给封装的Handler;要么自己返回一个响应,根本不让封装的Handler处理这次请求。然而,有些情况是这样的,不但基于请求触发一些动作,还要在封装的Handler处理后做一些扫尾工作,比如修改响应内容等。

添加数据比较容易

如果我们想在封装的handler输出的内容后添加一些数据,我们只需要在handler结束后继续调用Write()即可:

  1. type AppendMiddleware struct { 
  2.     handler http.Handler 
  3.   
  4. func (a *AppendMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { 
  5.     a.handler.ServeHTTP(w, r) 
  6.     w.Write([]byte("Middleware says hello.")) 

响应内容现在就应该包含封装的handler的内容,再加上Middleware says hello.

问题是

做其他的响应内容操作比较麻烦。比如,如果我们想在响应内容前写入一些数据。如果我们在封装的handler前调用Write(),那么封装的handler就好失去对HTTP状态码和HTTP头的控制。因为第一次调用Write()会直接将头输出。

想要修改原有输出(比如,替换其中的某些字符串),改变特定的HTTP头,设置不同的状态码也都因为同样的原因而不可行:当封装的handler返回时,上述数据早已被发送给客户端了。

为了处理这样的需求,我们需要一种特殊的可以用做bufferResponseWriter,它能够收集、暂存输出以用于修改等操作,最后再发送给客户端。我们可以将这个带bufferResponseWriter传给封装的handler,而不是真实的RW,这样就避免直接发送数据给客户端。

幸运的是,在Go标准库中确实存在这样一个工具。net/http/httptest中的ResponseRecorder就是这样的:它保存状态码,一个保存响应头的字典,将输出累计在buffer中。尽管是用于测试(这个包名暗示了这一点),它还是很好的满足了我们的需求。

让我们看一个使用ResponseRecorder的例子,这里修改了响应内容的所有东西,是为了更完整的演示。

  1. type ModifierMiddleware struct { 
  2.   
  3.     handler http.Handler 
  4.   
  5.   
  6. func (m *ModifierMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { 
  7.   
  8.     rec :httptest.NewRecorder() 
  9.   
  10.     // passing a ResponseRecorder instead of the original RW 
  11.   
  12.     m.handler.ServeHTTP(rec, r) 
  13.   
  14.     // after this finishes, we have the response recorded 
  15.   
  16.     // and can modify it before copying it to the original RW 
  17.   
  18.     // we copy the original headers first 
  19.   
  20.     for k, v :range rec.Header() { 
  21.   
  22.         w.Header()[k] = v 
  23.   
  24.     } 
  25.   
  26.     // and set an additional one 
  27.   
  28.     w.Header().Set("X-We-Modified-This", "Yup") 
  29.   
  30.     // only then the status code, as this call writes out the headers 
  31.   
  32.     w.WriteHeader(418) 
  33.   
  34.     // the body hasn't been written (to the real RW) yet, 
  35.   
  36.     // so we can prepend some data. 
  37.   
  38.     w.Write([]byte("Middleware says hello again. ")) 
  39.   
  40.     // then write out the original body 
  41.   
  42.     w.Write(rec.Body.Bytes()) 
  43.   

下面是我们包装的handler的输出。如果不用我们的中间件封装,原来的handler仅仅会输出Success!。

  1. HTTP/1.1 418 I'm a teapot 
  2.   
  3. X-We-Modified-This: Yup 
  4.   
  5. Content-Type: text/plain; charset=utf-8 
  6.   
  7. Content-Length: 37 
  8.   
  9. Date: Tue, 03 Sep 2013 18:41:39 GMT 
  10.   
  11. Middleware says hello again. Success! 

这种方式提供了非常大的便利。被封装的handler现在完全在我们的控制之下:即使在其返回之后,我们也可以以任意方式操作输出。

#p#

和其他handlers共享数据

在不同的情况下,中间件可以需要给其他的中间件或者应用程序暴露特定的信息。比如,nosurf需要给用户提供一种获取CSRF 密钥的方式以及错误原因(如果有错误的话)。

对这种需求,一个合适的模型就是使用一个隐藏的map,将http.Request指针指向需要的数据,然后暴露一个包级别(handler级别)的函数来访问这些数据。

我在nosurf中也使用了这种模型。这里,我创建了一个全局的上下文map。注意到,由于默认情况下Gomap不是并发访问安全的,需要一个mutex

  1. type csrfContext struct { 
  2.   
  3.     token string 
  4.   
  5.     reason error 
  6.   
  7.   
  8. var ( 
  9.   
  10.     contextMap = make(map[*http.Request]*csrfContext) 
  11.   
  12.     cmMutex    = new(sync.RWMutex) 
  13.   

使用handler设置数据,然后通过暴露的函数Token()来获取数据。

  1. func Token(req *http.Request) string { 
  2.   
  3.     cmMutex.RLock() 
  4.   
  5.     defer cmMutex.RUnlock() 
  6.   
  7.     ctx, ok :contextMap[req] 
  8.   
  9.     if !ok { 
  10.   
  11.             return "" 
  12.   
  13.     } 
  14.   
  15.     return ctx.token 
  16.   

你可以在nosurf的代码库context.go中找到完整的实现。

虽然我选择在nosurf中自己实现这种需求,但实际上存在一个handygorilla/context包,它实现了一个通用的保存请求信息的map。在大多数情况下,这个包足以满足你的需求,避免你在自己实现一个共享存储时踩坑。它甚至还有一个自己的中间件能在请求处理结束之后清除请求信息。

总结

这篇文章的目的是吸引Go用户对中间件概念的注意以及展示使用Go写中间件的一些基本组件。尽管Go是一个相对年轻的开发语言,Go拥有非常漂亮的标准HTTP接口。这也是用Go写中间件是个非常简单甚至快乐的过程的原因之一。

然而,目前Go仍然缺乏高质量的HTTP工具。我之前提到的Go中间件想法,大多都还没实现。现在你已经知道如何用Go写中间件了,为什么不自己做一个呢?

PS,你可以在一个GitHub gist中找到这篇文章中所有的中间件例子。

原文链接:http://justinas.org/writing-http-middleware-in-go/

译文链接:http://blog.jobbole.com/53265/

 

责任编辑:陈四芳 来源: 伯乐在线
相关推荐

2015-12-21 14:56:12

Go语言Http网络协议

2021-10-06 19:03:35

Go中间件Middleware

2022-11-18 07:54:02

Go中间件项目

2020-06-28 09:20:33

代码开发Go

2011-05-24 15:10:48

2021-02-11 08:21:02

中间件开发CRUD

2016-11-11 21:00:46

中间件

2018-07-29 12:27:30

云中间件云计算API

2018-02-01 10:19:22

中间件服务器系统

2023-06-29 10:10:06

Rocket MQ消息中间件

2023-10-24 07:50:18

消息中间件MQ

2009-06-16 15:55:06

JBoss企业中间件

2012-11-30 10:21:46

移动中间件

2024-02-06 14:05:00

Go中间件框架

2017-12-11 13:30:49

Go语言数据库中间件

2011-10-24 07:41:38

SOA中间件应用服务器

2009-06-16 10:53:01

JBoss中间件JBoss架构

2019-06-04 15:18:30

Web ServerNginx中间件

2021-12-14 10:39:12

中间件ActiveMQRabbitMQ

2021-04-22 06:13:41

Express 中间件原理中间件函数
点赞
收藏

51CTO技术栈公众号