重新探讨middleware

前面写过一篇中间件与责任链模式,最近被同事揪出来打了脸,感觉有必要再做一次学习和分析,下面就是新的学习成果~

中间件

让我们从例子开始,我们发现网站的评论系统中有人恶意进行xss攻击,因此想要对用户的请求做简单的xss过滤,简单地来做的话可以对所有用户提交内容中的尖括号进行过滤。那么可以完成形如下面的函数:

// input = `<script>alter(1)</script>script>`
func XSSClean(input string) {
    input = strings.Replace(a, `<`, `[filtered]`, -1)
    input = strings.Replace(a, `>`, `[filtered]`, -1)
}

把这个函数封装进我们自己的utils包中,在业务层需要进行xss过滤的时候只要使用XSSClean函数对输入字符串进行处理即可。这样做看起来问题不大,我们的业务代码会变成这个样子(伪代码):

func MyHandler(c *MyContext) {
    requestHttpBody := c.getRequestHttpBody
    requestHttpBody = XSSClean(requestHttpBody)

    //handle requestHttpBody
}

func MyHandler2(c *MyContext) {
    requestHttpBody := c.getRequestHttpBody
    requestHttpBody = XSSClean(requestHttpBody)

    //handle requestHttpBody
}

这样带来的问题非常明显,我们把业务代码和业务无关代码混在了一起。是一种“侵入式”的处理方式。侵入式的处理方式会在业务开发时造成较大的麻烦,例如我哪天变卦了,用户提交上来的内容不再进行展示,只是保存起来而已。这时候我想把之前的xss过滤功能从代码里移除,那么我就需要去修改所有的handler。程序员大概已经疯掉了。

有没有更好的处理方式呢?当然有。这就是中间件的存在价值了。web框架中间件,有些框架里可能会叫filter,是框架里比较重要的一环。

我们用net/http的标准库来举个例子。

package main

import "net/http"

func middlewareHandler(next http.Handler) http.Handler {
	// 使用HandlerFunc包装自己
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("do sth before handle\n"))
		next.ServeHTTP(w, r)
		w.Write([]byte("do sth after handle\n"))
	})
}

func main() {
	var mainHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("handle user request\n"))
	})

	mergedHandler := middlewareHandler(mainHandler)
	http.ListenAndServe(":8080", mergedHandler)
}

在mac或者linux上输入curl localhost:8080,可以得到这个demo的输出。

~ ❯❯❯ curl localhost:8080
do sth before handle
handle user request
do sth after handle

有了这个手段,我们就可以给任意的http.Handler加上自己想要的hook,并且可以是前置的hook、后置的hook,或者包围式的hook,我们以非业务需求举一些常见的例子:

权限验证 => 前置
xss过滤 => 前置
接口耗时统计 => 包围式
ratelimit => 前置
panic recovery => 勉强算后置吧
监控数据上报 => 后置

前面的demo里举的是包围式的例子,我们看看前置的中间件的例子,这里只列不同的部分的代码:

...省略...

func middlewareHandler(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("do sth before handle\n"))
		next.ServeHTTP(w, r)
	})
}

...省略...

还像上面一样,curl 8080端口,输出:

do sth before handle
handle user request

一口气,再来个后置的例子:

...省略...

func middlewareHandler(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		next.ServeHTTP(w, r)
		w.Write([]byte("do sth after handle\n"))
	})
}

...省略...

curl 8080端口,输出:

handle user request
do sth after handle

这样看上去区别就是显而易见的了。因为前置和后置比较好懂,我们来思考一下包围式中间件的运行原理。说是思考,实际上也很简单,这个和基本的递归函数压栈的过程其实是完全一样的:

call middleware
||
push stack middleware context, call handler
||
pop stack, get middleware context, continue logic in middleware

很简单的三个过程,理解了原理之后,我们来看一看加强版的例子。

package main

import "net/http"

func requestLogger(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
		next.ServeHTTP(w, r)
        logger.Printf("Completed in %v", time.Since(start))
	})
}

// 多包裹一层,就可以使得middleware本身支持参数了
func corsFilter(corsSetting string) {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
             w.Header().Set("Access-Control-Allow-Origin", corsSetting)
             next.ServeHTTP(w, r)
        })
    }
}

var handlers []http.Handler

func mergeHandlers() http.Handler {
    var finalHandler http.Handler
    finalHandler = handlers[len(handlers) - 1]
    for i:= len(handlers) - 2; i >= 0;i-- {
        // 相当于一个手动压栈的过程
        finalHandler = handlers[i](finalHandler)
    }

    return finalHandler
}

func main() {
	var mainHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("handle user request\n"))
	})

    // 为一个handler增加,requestLogger和支持跨域的filter
    handlers = append(handlers, requestLogger)
    // 注意这里传入的参数
    handlers = append(handlers, corsFilter("*"))
    handlers = append(handlers, mainHandler)

	mergedHandler := middlewareHandler(mainHandler)
	http.ListenAndServe(":8080", mergedHandler)
}

这样我们就支持了下面的两个特性:

1.多个middleware嵌套,这样就可以实现形如mw1 => mw2 => mw3 => mw4 => mw5 => handler => mw5 => mw4 => mw3 => mw2 => mw1这样的链式调用

2.为middleware定制参数,例如例子中corsFilter

在不考虑复杂路由的情况下,你已经具备了基于net/http写一个简单的web框架的能力。事实上,negroni这个项目也就是基于这种标准net/http外加中间件的思路而做成的一个项目。

除了使用标准的net/http库之外,市面上的其它web框架也不少,不过中间件的思路大抵都是如此的。理解了本文便可以举一反三。将本文的例子再进行一下强化,我们可以给中间件进行一些分组,例如有些中间件是全局的中间件,而另一些是挂在特定的路由上中间件,这样便可以对请求进行更细粒度的灵活控制。感兴趣的读者可以阅读一下gin的middleware相关的代码,就是这么做的。

说了这么多,为什么我说否定了上一篇的结论呢?

因为中间件的设计不仅仅是简单的pipeline模式啊。。因为有包围式的需求,所以需要在进入调用链之后还要进行回溯(m2 before part=> handler =>m2 after part),这样就没有办法用简单的pipeline模式来实现了。考虑到web框架中的middleware也不会真的像那篇里讲的那么夸张,一次挂几万个,所以这种剥洋葱式的调用也就无所谓了~

Xargin

Xargin

If you don't keep moving, you'll quickly fall behind
Beijing