用Go語(yǔ)言寫(xiě)HTTP中間件
在web開(kāi)發(fā)過(guò)程中,中間件一般是指應(yīng)用程序中封裝原始信息,添加額外功能的組件。不知道為什么,中間件通常是一種不太受歡迎的概念。但我認(rèn)為它棒極了。
其一,一個(gè)好的中間件擁有單一的功能,可插拔并且是自我約束的。這就意味著你可以在接口的層次上把它放到應(yīng)用中,并能很好的工作。中間件并不影響你 的代碼風(fēng)格,它也不是一個(gè)框架,僅僅是你處理請(qǐng)求流程中額外一層罷了。根本不需要重寫(xiě)代碼:如果你想用一個(gè)中間件,就把它加上應(yīng)用中;如果你改變主意了, 去掉就好了。就這么簡(jiǎn)單。
來(lái)看看Go,HTTP中間件非常流行,標(biāo)準(zhǔn)庫(kù)中也是這樣。或許咋看上去并不明顯,net/http包中的函數(shù),如StripPrefix 和TimeoutHandler 正是我們上面定義的中間件:封裝處理過(guò)程并在處理輸入或輸出時(shí)增加額外的動(dòng)作。
我最近的Go包nosurf 也是一個(gè)中間件。我從一開(kāi)始就有意的這樣設(shè)計(jì)。大多數(shù)情況下,你根本不必在應(yīng)用層關(guān)心CSRF檢查。nosurf,和其他中間件一樣,非常獨(dú)立,可以和實(shí)現(xiàn)標(biāo)準(zhǔn)庫(kù)net/http接口的工具配合使用。
你也可以使用中間件做這些:
- 通過(guò)隱藏長(zhǎng)度緩解BREACH攻擊
- 頻率限制
- 屏蔽惡意自動(dòng)程序
- 提供調(diào)試信息
- 添加HSTS, X-Frame-Options頭
- 從異常中優(yōu)雅恢復(fù)
- 以及其他等等。
寫(xiě)一個(gè)簡(jiǎn)單的中間件
第一個(gè)例子中,我寫(xiě)了一個(gè)中間件,只允許用戶(hù)從特定的域(在HTTP的Host頭中有域信息)來(lái)訪問(wèn)服務(wù)器。這樣的中間件可以保護(hù)應(yīng)用程序不受“主機(jī)欺騙攻擊”
定義類(lèi)型
為了方便,讓我們?yōu)檫@個(gè)中間件定義一種類(lèi)型,叫做SingleHost。
- type SingleHost struct {
- handler http.Handler
- allowedHost string
- }
只包含兩個(gè)字段:
- 封裝的Handler。如果是有效的Host訪問(wèn),我們就調(diào)用這個(gè)Handler。
- 允許的主機(jī)值。
由于我們把字段名小寫(xiě)了,使得該字段只對(duì)我們自己的包可見(jiàn)。我們還應(yīng)該寫(xiě)一個(gè)初始化函數(shù)。
- func NewSingleHost(handler http.Handler, allowedHost string) *SingleHost {
- return &SingleHost{handler: handler, allowedHost: allowedHost}
- }
處理請(qǐng)求
現(xiàn)在才是實(shí)際的邏輯。為了實(shí)現(xiàn)http.Handler,我們的類(lèi)型秩序?qū)崿F(xiàn)一個(gè)方法:
- type Handler interface {
- ServeHTTP(ResponseWriter, *Request)
- }
這就是我們實(shí)現(xiàn)的方法:
- func (s *SingleHost) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- host := r.Host
- if host == s.allowedHost {
- s.handler.ServeHTTP(w, r)
- } else {
- w.WriteHeader(403)
- }
- }
ServeHTTP 函數(shù)僅僅檢查請(qǐng)求中的Host頭:
- 如果Host頭匹配初始化函數(shù)設(shè)置的allowedHost ,就調(diào)用封裝handler的ServeHTTP方法。
- 如果Host頭不匹配,就返回403狀態(tài)碼(禁止訪問(wèn))。
在后一種情況中,封裝handler的ServeHTTP方法根本就不會(huì)被調(diào)用。因此封裝的handler根本不會(huì)有任何輸出,實(shí)際上它根本就不知道有這樣一個(gè)請(qǐng)求到來(lái)。
現(xiàn)在我們已經(jīng)完成了自己的中間件,來(lái)把它放到應(yīng)用中。這次我們不把Handler直接放到net/http服務(wù)中,而是先把Handler封裝到中間件中。
- singleHosted = NewSingleHost(myHandler, "example.com")
- http.ListenAndServe(":8080", singleHosted)
另外一種方法
我們剛才寫(xiě)的中間件實(shí)在是太簡(jiǎn)單了,只有僅僅15行代碼。為了寫(xiě)這樣的中間件,引入了一個(gè)不太通用的方法。由于Go支持函數(shù)第一型和閉包,并且擁有簡(jiǎn)潔的http.HandlerFunc包裝器,我們可以將其實(shí)現(xiàn)為一個(gè)簡(jiǎn)單的函數(shù),而不是寫(xiě)一個(gè)單獨(dú)的類(lèi)型。下面是基于函數(shù)的中間件版本。
- func SingleHost(handler http.Handler, allowedHost string) http.Handler {
- ourFunc := func(w http.ResponseWriter, r *http.Request) {
- host := r.Host
- if host == allowedHost {
- handler.ServeHTTP(w, r)
- } else {
- w.WriteHeader(403)
- }
- }
- return http.HandlerFunc(ourFunc)
- }
#p#
這里我們聲明了一個(gè)叫做SingleHost的簡(jiǎn)單函數(shù),接受一個(gè)Handler和允許的主機(jī)名。在函數(shù)內(nèi)部,我們創(chuàng)建了一個(gè)類(lèi)似之前版本ServeHTTP的函數(shù)。這個(gè)內(nèi)部函數(shù)其實(shí)是一個(gè)閉包,所以它可以從SingleHost外部訪問(wèn)。最終,我們通過(guò)HandlerFunc把這個(gè)函數(shù)用作http.Handler。
使用Handler還是定義一個(gè)http.Handler類(lèi)型完全取決于你。對(duì)簡(jiǎn)單的情況而已,一個(gè)函數(shù)就足夠了。但是隨著中間件功能的復(fù)雜,你應(yīng)該考慮定義自己的數(shù)據(jù)結(jié)構(gòu),把邏輯獨(dú)立到多個(gè)方法中。
實(shí)際上,標(biāo)準(zhǔn)庫(kù)這兩種方法都用了。StripPrefix 是一個(gè)返回HandlerFunc的函數(shù)。雖然TimeoutHandler也是一個(gè)函數(shù),但它返回了處理請(qǐng)求的自定義的類(lèi)型。
更復(fù)雜的情況
我們的SingleHost中間件非常簡(jiǎn)單:先檢查請(qǐng)求的一個(gè)屬性,然后要么什么也不管,把請(qǐng)求直接傳給封裝的Handler;要么自己返回一個(gè)響應(yīng),根本不讓封裝的Handler處理這次請(qǐng)求。然而,有些情況是這樣的,不但基于請(qǐng)求觸發(fā)一些動(dòng)作,還要在封裝的Handler處理后做一些掃尾工作,比如修改響應(yīng)內(nèi)容等。
添加數(shù)據(jù)比較容易
如果我們想在封裝的handler輸出的內(nèi)容后添加一些數(shù)據(jù),我們只需要在handler結(jié)束后繼續(xù)調(diào)用Write()即可:
- type AppendMiddleware struct {
- handler http.Handler
- }
- func (a *AppendMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- a.handler.ServeHTTP(w, r)
- w.Write([]byte("Middleware says hello."))
- }
響應(yīng)內(nèi)容現(xiàn)在就應(yīng)該包含封裝的handler的內(nèi)容,再加上Middleware says hello.
問(wèn)題是
做其他的響應(yīng)內(nèi)容操作比較麻煩。比如,如果我們想在響應(yīng)內(nèi)容前寫(xiě)入一些數(shù)據(jù)。如果我們?cè)诜庋b的handler前調(diào)用Write(),那么封裝的handler就好失去對(duì)HTTP狀態(tài)碼和HTTP頭的控制。因?yàn)榈谝淮握{(diào)用Write()會(huì)直接將頭輸出。
想要修改原有輸出(比如,替換其中的某些字符串),改變特定的HTTP頭,設(shè)置不同的狀態(tài)碼也都因?yàn)橥瑯拥脑蚨豢尚校寒?dāng)封裝的handler返回時(shí),上述數(shù)據(jù)早已被發(fā)送給客戶(hù)端了。
為了處理這樣的需求,我們需要一種特殊的可以用做buffer的ResponseWriter,它能夠收集、暫存輸出以用于修改等操作,最后再發(fā)送給客戶(hù)端。我們可以將這個(gè)帶buffer的ResponseWriter傳給封裝的handler,而不是真實(shí)的RW,這樣就避免直接發(fā)送數(shù)據(jù)給客戶(hù)端。
幸運(yùn)的是,在Go標(biāo)準(zhǔn)庫(kù)中確實(shí)存在這樣一個(gè)工具。net/http/httptest中的ResponseRecorder就是這樣的:它保存狀態(tài)碼,一個(gè)保存響應(yīng)頭的字典,將輸出累計(jì)在buffer中。盡管是用于測(cè)試(這個(gè)包名暗示了這一點(diǎn)),它還是很好的滿(mǎn)足了我們的需求。
讓我們看一個(gè)使用ResponseRecorder的例子,這里修改了響應(yīng)內(nèi)容的所有東西,是為了更完整的演示。
- type ModifierMiddleware struct {
- handler http.Handler
- }
- func (m *ModifierMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- rec := httptest.NewRecorder()
- // passing a ResponseRecorder instead of the original RW
- m.handler.ServeHTTP(rec, r)
- // after this finishes, we have the response recorded
- // and can modify it before copying it to the original RW
- // we copy the original headers first
- for k, v := range rec.Header() {
- w.Header()[k] = v
- }
- // and set an additional one
- w.Header().Set("X-We-Modified-This", "Yup")
- // only then the status code, as this call writes out the headers
- w.WriteHeader(418)
- // the body hasn't been written (to the real RW) yet,
- // so we can prepend some data.
- w.Write([]byte("Middleware says hello again. "))
- // then write out the original body
- w.Write(rec.Body.Bytes())
- }
下面是我們包裝的handler的輸出。如果不用我們的中間件封裝,原來(lái)的handler僅僅會(huì)輸出Success!。
- HTTP/1.1 418 I'm a teapot
- X-We-Modified-This: Yup
- Content-Type: text/plain; charset=utf-8
- Content-Length: 37
- Date: Tue, 03 Sep 2013 18:41:39 GMT
- Middleware says hello again. Success!
這種方式提供了非常大的便利。被封裝的handler現(xiàn)在完全在我們的控制之下:即使在其返回之后,我們也可以以任意方式操作輸出。
#p#
和其他handlers共享數(shù)據(jù)
在不同的情況下,中間件可以需要給其他的中間件或者應(yīng)用程序暴露特定的信息。比如,nosurf需要給用戶(hù)提供一種獲取CSRF 密鑰的方式以及錯(cuò)誤原因(如果有錯(cuò)誤的話(huà))。
對(duì)這種需求,一個(gè)合適的模型就是使用一個(gè)隱藏的map,將http.Request指針指向需要的數(shù)據(jù),然后暴露一個(gè)包級(jí)別(handler級(jí)別)的函數(shù)來(lái)訪問(wèn)這些數(shù)據(jù)。
我在nosurf中也使用了這種模型。這里,我創(chuàng)建了一個(gè)全局的上下文map。注意到,由于默認(rèn)情況下Go的map并不是并發(fā)訪問(wèn)安全的,需要一個(gè)mutex。
- type csrfContext struct {
- token string
- reason error
- }
- var (
- contextMap = make(map[*http.Request]*csrfContext)
- cmMutex = new(sync.RWMutex)
- )
使用handler設(shè)置數(shù)據(jù),然后通過(guò)暴露的函數(shù)Token()來(lái)獲取數(shù)據(jù)。
- func Token(req *http.Request) string {
- cmMutex.RLock()
- defer cmMutex.RUnlock()
- ctx, ok := contextMap[req]
- if !ok {
- return ""
- }
- return ctx.token
- }
你可以在nosurf的代碼庫(kù)context.go中找到完整的實(shí)現(xiàn)。
雖然我選擇在nosurf中自己實(shí)現(xiàn)這種需求,但實(shí)際上存在一個(gè)handygorilla/context包,它實(shí)現(xiàn)了一個(gè)通用的保存請(qǐng)求信息的map。在大多數(shù)情況下,這個(gè)包足以滿(mǎn)足你的需求,避免你在自己實(shí)現(xiàn)一個(gè)共享存儲(chǔ)時(shí)踩坑。它甚至還有一個(gè)自己的中間件能在請(qǐng)求處理結(jié)束之后清除請(qǐng)求信息。
總結(jié)
這篇文章的目的是吸引Go用戶(hù)對(duì)中間件概念的注意以及展示使用Go寫(xiě)中間件的一些基本組件。盡管Go是一個(gè)相對(duì)年輕的開(kāi)發(fā)語(yǔ)言,Go擁有非常漂亮的標(biāo)準(zhǔn)HTTP接口。這也是用Go寫(xiě)中間件是個(gè)非常簡(jiǎn)單甚至快樂(lè)的過(guò)程的原因之一。
然而,目前Go仍然缺乏高質(zhì)量的HTTP工具。我之前提到的Go中間件想法,大多都還沒(méi)實(shí)現(xiàn)。現(xiàn)在你已經(jīng)知道如何用Go寫(xiě)中間件了,為什么不自己做一個(gè)呢?
PS,你可以在一個(gè)GitHub gist中找到這篇文章中所有的中間件例子。
原文鏈接:http://justinas.org/writing-http-middleware-in-go/
譯文鏈接:http://blog.jobbole.com/53265/