再web開發的背景下,“中間件”通常意思是“包裝原始應用並添加一些額外的功能的應用的一部分”。這個概念似乎總是不被人理解,但是我認為中間件非常棒。
首先,一個好的中間件有一個責任就是可插拔並且自足。這就意味着你可以在接口級別嵌入你的中間件他就能直接運行。它不會影響你編碼方式,不是框架,僅僅是你請求處理里面的一層而已。完全沒必要重寫你的代碼,如果你想使用中間件的一個功能,你就幫他插入到那里,如果不想使用了,就可以直接移除。
縱觀Go語言,中間件是非常普遍的,即使在標准庫中。雖然開始的時候不會那么明顯,在標准庫net/http
中的函數StripText
或者TimeoutHandler
就是我們要定義和的中間件的樣子,處理請求和相應的時候他們包裝你的handler,並處理一些額外的步驟。
我最近寫的Go包nosurf同樣也是個中間件。我特意將他從頭開始設計。在大多數情況下,你不需要在應用層擔心CSRF攻擊,nosurf像其他的中間件一樣可以自足,並且和net/http
的接口無縫銜接。
同樣你還可以使用中間件做:
- 隱藏長度防止緩沖攻擊
- 速度限制
- 屏蔽爬蟲
- 提供調試信息
- 添加HSTS,X-Frame-Options頭
- 從錯誤中恢復
- 等等
編寫一個簡單的中間件
我們的第一個例子是寫一個只允許一個域名下的用戶訪問的中間件,通過HTTP的HOST
header實現。這樣的中間件可以防止主機欺騙攻擊。
類型的機構
首先我們定義一個結構體,叫做SingleHost
type SingleHost struct { handler http.Handler allowedHost string }
它只包含兩個field。
- 如果是一個可用的Host,那么我們會調用嵌入的handler。
- allowedHost 就是允許的Host。
因為我們將其首字母小寫,因此他們只對本包可見。我們需要給它定義已給構造函數。
func NewSingleHost(handler http.Handler, allowedHost string) *SingleHost { return &SingleHost{handler: handler, allowedHost: allowedHost} }
請求處理
現在需要實現真正的邏輯功能了。想要實現http.Handler
,我們只需要實現他的一個方法。
type Handler interface { ServeHTTP(ResponseWriter, *Request) }
實現如下:
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
只是檢查請求的Host:
- 如果Host和配置的allowed一直,那么調用handler的ServeHTTP。
- 如果不一直返回403
對於后一種情況,不僅不會得到應答,設置不知道有這個請求。
現在我們已經開發哈了中間件,只需要將其插入到需要的地方。
singleHosted = NewSingleHost(myHandler, "example.com") http.ListenAndServe(":8080", singleHosted)
另一種方式
我們剛剛寫的那個中間件很簡單,它只有15行代碼。寫這樣的中間件,可以使用樣板方法。由於Go支持函數為一等公民和閉包,並且有http.HandlerFunc
包裝函數,我們可以通過他創建一個中間件,而不是將其放入到一個結構體中。下面是這個中間件的寫法。
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) }
我們定義了一個簡單的函數SingleHost
,它包裝了Handler
和允許的Host,在其內部我們實現了一個跟上面中間件類似的功能。我們內部的函數就是一個閉包,因此他可以訪問外部函數的變量。最終HandlerFunc讓我們可以將其變為Handler。
覺得是使用HandlerFunc還是自己實現一個http.Handler完全取決於你自己。對於簡單的情況,一個簡單的函數就完全夠了。如果你的中間件越來越多,那么就可以考慮實現自己的結構並把它們分開。
同時標准庫同時使用了兩種功能。StripPrefix
使用的是HandlerFunc,TimeoutHandler使用的是自定義的結構體。
一個更復雜的例子
我們的SingleHost
並不重要,我們只檢測一個屬性,要么將他傳遞給其他的handler,要么直接返回。然而存在這種情況,我們的程序需要對處理完進行后續處理。
添加數據是簡單的
如果只是想簡單的添加數據,那么使用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.")) }
返回的結構肯定會包含Middleware says hello.
問題
但是操作其他的數據有點困難。例如我們想要在它前面添加數據而不是后面追加。如果我們在原Handler之前調用Write,那么將會失去控制,因為第一個Write已經將他寫入了。
通過其他方法修改原始輸出,例如替換字符串,改變響應header,或者設置狀態碼都不會起作用,因為當handler返回的時候數據已經返回到客戶端了。
為了實現這個功能,我們需要一個特殊的ResponseWriter,他可以想buffer一樣工作,收集數據,存儲以備使用和修改。然后我們將這個ResponseWriter傳遞給handler,而不是傳遞真是的RW,這樣在其之前我們已經修改了它了。
幸運的是在標准庫中有這樣的一個工具。在net/http/httptest
包里的ResponseRecorder能做所有我們需要的:保存狀態碼,一個響應header的map,將body放入byte 緩沖中。雖然它是再測試中中使用的,但是很服務我們的情況。
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. data := []byte("Middleware says hello again. ") // But the Content-Length might have been set already, // we should modify it by adding the length // of our own data. // Ignoring the error is fine here: // if Content-Length is empty or otherwise invalid, // Atoi() will return zero, // which is just what we'd want in that case. clen, _ := strconv.Atoi(r.Header.Get("Content-Length")) clen += len(data) r.Header.Set("Content-Length", strconv.Itoa(clen)) // finally, write out our data w.Write(data) // then write out the original body w.Write(rec.Body.Bytes()) }
我們中間件的輸出:
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完全手控制。
和其他handler分享數據
在其他例子中,中間件可能需要暴露一些信息給其他中間件或者應用本身。例如nosurf需要給其他用戶訪問CSRF token的權限。
最簡單是是使用一個map,但是通常不希望這樣。它將http.Request 的指針作為key,其他數據作為value。下面是nosurf的例子,Go的map非線程安全,所以要自己是實現。
type csrfContext struct { token string reason error } var ( contextMap = make(map[*http.Request]*csrfContext) cmMutex = new(sync.RWMutex) )
func Token(req *http.Request) string { cmMutex.RLock() defer cmMutex.RUnlock() ctx, ok := contextMap[req] if !ok { return "" } return ctx.token }