转载自 https://www.alexedwards.net/blog/making-and-using-middleware
当你构建一个web应用程序时,可能有一些共享的功能,你想参加许多(甚至是全部)HTTP请求。 您可能想要记录每个请求,gzip每个响应,或做一些繁重的处理之前检查缓存。
组织这个共享功能的一种方法是设置它 中间件 ——独立的代码独立作用于正常的应用程序请求之前或之后处理程序。 在一个共同的地方去使用ServeMux之间的中间件和应用程序处理程序,以便控制流为一个HTTP请求的样子:
ServeMux => Middleware Handler => Application Handler
在这篇文章中,我将解释如何使自定义中间件,在此模式中,通过一些具体的例子以及运行使用第三方中间件包。
基本原则
制作和使用中间件在根本上是简单的。 我们希望:
- 实现我们的中间件,使它满足 http.Handler 接口。
- 建立一个 链的处理程序 包含我们的中间件处理程序和正常的应用处理程序,我们可以注册一个 http.ServeMux 。
希望你已经熟悉下面的方法构建一个处理程序(如果不是,最好读 https://www.alexedwards.net/blog/a-recap-of-request-handling)。
func messageHandler(message string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(message)
})
}
在这个处理程序(一个简单的我们将逻辑 w.Write )在一个匿名函数和closing-over message 变量来形成一个闭包。 我们然后将这个闭包转换为一个处理程序使用 http.HandlerFunc 适配器并返回它。
我们可以用同样的方法来创建一个处理程序链。 而不是一个字符串传递到闭包(如上图)我们可以通过 链中的下一个处理程序 作为一个变量,然后将控制权移交给下一个处理程序通过调用它 ServeHTTP() 方法。
这给了我们一个完整的模式构建中间件:
func exampleMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Our middleware logic goes here...
next.ServeHTTP(w, r)
})
}
你会注意到这个中间件功能 func(http.Handler)http.Handler 签名。 它接受一个处理程序作为参数,并返回一个处理程序。 这是有用的,有两个原因:
- 因为它返回一个处理程序我们可以注册中间件功能直接与提供的标准ServeMux net/http包。
- 我们可以创建一个任意长度的处理程序链嵌套中间件功能在每个其他。 例如:http.Handle("/", middlewareOne(middlewareTwo(finalHandler)))
控制流的说明
让我们来看看一个精简的例子和一些中间件,简单地将日志消息写入标准输出:
文件:main.go
package main
import (
"log"
"net/http"
)
func middlewareOne(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println("Executing middlewareOne")
next.ServeHTTP(w, r)
log.Println("Executing middlewareOne again")
})
}
func middlewareTwo(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println("Executing middlewareTwo")
if r.URL.Path != "/" {
return
}
next.ServeHTTP(w, r)
log.Println("Executing middlewareTwo again")
})
}
func final(w http.ResponseWriter, r *http.Request) {
log.Println("Executing finalHandler")
w.Write([]byte("OK"))
}
func main() {
finalHandler := http.HandlerFunc(final)
http.Handle("/", middlewareOne(middlewareTwo(finalHandler)))
http.ListenAndServe(":3000", nil)
}
运行该应用程序的请求 http://localhost:3000 。 你应该得到类似的日志输出:
$ go run main.go
2014/10/13 20:27:36 Executing middlewareOne
2014/10/13 20:27:36 Executing middlewareTwo
2014/10/13 20:27:36 Executing finalHandler
2014/10/13 20:27:36 Executing middlewareTwo again
2014/10/13 20:27:36 Executing middlewareOne again
很明显看到如何通过控制处理程序链的顺序嵌套,然后再回来 反方向 。
我们可以停止控制传播链在任何时候通过发行 返回 从一个中间件处理程序。
在上面的示例中我已经包括了一个条件返回的 middlewareTwo 函数。 尝试通过访问 http://localhost:3000 / foo 并再次检查日志,你会发现这一次的请求没有得到进一步的比 middlewareTwo 之前传递链。
一个适当的例子
好,假设我们正在构建一个服务进程请求包含一个XML的身体。 我们希望创建一些中间件的)检查请求主体的存在,和b)检测body以确保它是XML。 如果检查失败,我们希望我们的中间件来写一个错误消息并停止请求到达应用程序处理程序。
文件:main.go
package main
import (
"bytes"
"net/http"
)
func enforceXMLHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check for a request body
if r.ContentLength == 0 {
http.Error(w, http.StatusText(400), 400)
return
}
// Check its MIME type
buf := new(bytes.Buffer)
buf.ReadFrom(r.Body)
if http.DetectContentType(buf.Bytes()) != "text/xml; charset=utf-8" {
http.Error(w, http.StatusText(415), 415)
return
}
next.ServeHTTP(w, r)
})
}
func main() {
finalHandler := http.HandlerFunc(final)
http.Handle("/", enforceXMLHandler(finalHandler))
http.ListenAndServe(":3000", nil)
}
func final(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
}
这看起来不错。 让我们来测试它通过创建一个简单的XML文件:
$ cat > books.xml
H. G. Wells
The Time Machine
8.50
使用cURL和做一些请求:
$ curl -i localhost:3000
HTTP/1.1 400 Bad Request
Content-Type: text/plain; charset=utf-8
Content-Length: 12
Bad Request
$ curl -i -d "This is not XML" localhost:3000
HTTP/1.1 415 Unsupported Media Type
Content-Type: text/plain; charset=utf-8
Content-Length: 23
Unsupported Media Type
$ curl -i -d @books.xml localhost:3000
HTTP/1.1 200 OK
Date: Fri, 17 Oct 2014 13:42:10 GMT
Content-Length: 2
Content-Type: text/plain; charset=utf-8
OK
使用第三方中间件
而不是一直在自己的中间件的滚你可能想要使用一个第三方包。 我们要看看几个: goji/httpauth 和 Gorilla's LoggingHandler 。
goji/httpauth包提供了HTTP基本身份验证功能。 它有一个 SimpleBasicAuth 辅助它返回一个函数的签名 func(http.Handler)http.Handler 。 这意味着我们可以在完全相同的方式使用它作为我们的定制中间件。
$ go get github.com/goji/httpauth
文件:main.go
package main
import (
"github.com/goji/httpauth"
"net/http"
)
func main() {
finalHandler := http.HandlerFunc(final)
authHandler := httpauth.SimpleBasicAuth("username", "password")
http.Handle("/", authHandler(finalHandler))
http.ListenAndServe(":3000", nil)
}
func final(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
}
如果你运行这个例子,你应该得到你所期望的反应的有效和无效凭证:
$ curl -i username:password@localhost:3000
HTTP/1.1 200 OK
Content-Length: 2
Content-Type: text/plain; charset=utf-8
OK
$ curl -i username:wrongpassword@localhost:3000
HTTP/1.1 401 Unauthorized
Content-Type: text/plain; charset=utf-8
Www-Authenticate: Basic realm=""Restricted""
Content-Length: 13
Unauthorized
go get github.com/gorilla/handlers
文件:main.go
package main
import (
"github.com/gorilla/handlers"
"net/http"
"os"
)
func main() {
finalHandler := http.HandlerFunc(final)
logFile, err := os.OpenFile("server.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
panic(err)
}
http.Handle("/", handlers.LoggingHandler(logFile, finalHandler))
http.ListenAndServe(":3000", nil)
}
func final(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
}
在这样一个微不足道的情况下我们的代码是相当清楚的。 但是如果我们想使用LoggingHandler中间件链的一部分吗? 我们可以很容易地得到一个声明是这样的……
http.Handle("/", handlers.LoggingHandler(logFile, authHandler(enforceXMLHandler(finalHandler))))
弄清楚的一个方法是通过创建一个构造函数(我们叫它 myLoggingHandler )签名 func(http.Handler)http.Handler 。 这将允许我们与其他中间件:巢更整齐
func myLoggingHandler(h http.Handler) http.Handler {
logFile, err := os.OpenFile("server.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
panic(err)
}
return handlers.LoggingHandler(logFile, h)
}
func main() {
finalHandler := http.HandlerFunc(final)
http.Handle("/", myLoggingHandler(finalHandler))
http.ListenAndServe(":3000", nil)
}
如果你运行这个应用程序和做一些请求 server.log 文件应该是这样的:
$ cat server.log
127.0.0.1 - - [21/Oct/2014:18:56:43 +0100] "GET / HTTP/1.1" 200 2
127.0.0.1 - - [21/Oct/2014:18:56:36 +0100] "POST / HTTP/1.1" 200 2
127.0.0.1 - - [21/Oct/2014:18:56:43 +0100] "PUT / HTTP/1.1" 200 2
如果你感兴趣,这是一个要点 三种中间件处理程序 从这篇文章结合一个例子。
边注:注意 Gorilla LoggingHandler记录响应状态( 200 )和响应的长度( 2) 日志中。 这是有趣的。 upstream 中间件怎么知道响应主体由我们的应用处理程序吗?
它通过定义它自己的 responseLogger 类型的包装 http.ResponseWriter ,创建自定义 responseLogger.Write() 和 responseLogger.WriteHeader() 方法。 这些方法不仅写响应,而且存储大小和地位,以便日后检查。 Gorilla LoggingHandler传递 responseLogger 到链中的下一个处理程序,而不是正常的 http.ResponseWriter 。
额外的工具
https://github.com/justinas/alice
Alice by Justinas Stankevičius 是一个聪明的和非常轻量级的包提供了一些链接中间件处理程序的语法糖。 在最基本的Alice让你重写这个:
http.Handle("/", myLoggingHandler(authHandler(enforceXMLHandler(finalHandler))))
是这样的:
http.Handle("/", alice.New(myLoggingHandler, authHandler, enforceXMLHandler).Then(finalHandler))
至少在我眼里,这段代码稍微清晰的理解。 但是,Alice 真正的好处是,它允许您指定一个处理程序链一旦和重用它为多个路线。 像这样:
stdChain := alice.New(myLoggingHandler, authHandler, enforceXMLHandler)
http.Handle("/foo", stdChain.Then(fooHandler))
http.Handle("/bar", stdChain.Then(barHandler))