新聞中心
在web開發(fā)過程中,中間件一般是指應(yīng)用程序中封裝原始信息,添加額外功能的組件。不知道為什么,中間件通常是一種不太受歡迎的概念。但我認(rèn)為它棒極了。

成都創(chuàng)新互聯(lián)是一家集網(wǎng)站建設(shè),山東企業(yè)網(wǎng)站建設(shè),山東品牌網(wǎng)站建設(shè),網(wǎng)站定制,山東網(wǎng)站建設(shè)報價,網(wǎng)絡(luò)營銷,網(wǎng)絡(luò)優(yōu)化,山東網(wǎng)站推廣為一體的創(chuàng)新建站企業(yè),幫助傳統(tǒng)企業(yè)提升企業(yè)形象加強企業(yè)競爭力。可充分滿足這一群體相比中小企業(yè)更為豐富、高端、多元的互聯(lián)網(wǎng)需求。同時我們時刻保持專業(yè)、時尚、前沿,時刻以成就客戶成長自我,堅持不斷學(xué)習(xí)、思考、沉淀、凈化自己,讓我們?yōu)楦嗟钠髽I(yè)打造出實用型網(wǎng)站。
其一,一個好的中間件擁有單一的功能,可插拔并且是自我約束的。這就意味著你可以在接口的層次上把它放到應(yīng)用中,并能很好的工作。中間件并不影響你 的代碼風(fēng)格,它也不是一個框架,僅僅是你處理請求流程中額外一層罷了。根本不需要重寫代碼:如果你想用一個中間件,就把它加上應(yīng)用中;如果你改變主意了, 去掉就好了。就這么簡單。
來看看Go,HTTP中間件非常流行,標(biāo)準(zhǔn)庫中也是這樣?;蛟S咋看上去并不明顯,net/http包中的函數(shù),如StripPrefix 和TimeoutHandler 正是我們上面定義的中間件:封裝處理過程并在處理輸入或輸出時增加額外的動作。
我最近的Go包nosurf 也是一個中間件。我從一開始就有意的這樣設(shè)計。大多數(shù)情況下,你根本不必在應(yīng)用層關(guān)心CSRF檢查。nosurf,和其他中間件一樣,非常獨立,可以和實現(xiàn)標(biāo)準(zhǔn)庫net/http接口的工具配合使用。
你也可以使用中間件做這些:
- 通過隱藏長度緩解BREACH攻擊
- 頻率限制
- 屏蔽惡意自動程序
- 提供調(diào)試信息
- 添加HSTS, X-Frame-Options頭
- 從異常中優(yōu)雅恢復(fù)
- 以及其他等等。
寫一個簡單的中間件
第一個例子中,我寫了一個中間件,只允許用戶從特定的域(在HTTP的Host頭中有域信息)來訪問服務(wù)器。這樣的中間件可以保護(hù)應(yīng)用程序不受“主機欺騙攻擊”
定義類型
為了方便,讓我們?yōu)檫@個中間件定義一種類型,叫做SingleHost。
- type SingleHost struct {
- handler http.Handler
- allowedHost string
- }
只包含兩個字段:
- 封裝的Handler。如果是有效的Host訪問,我們就調(diào)用這個Handler。
- 允許的主機值。
由于我們把字段名小寫了,使得該字段只對我們自己的包可見。我們還應(yīng)該寫一個初始化函數(shù)。
- func NewSingleHost(handler http.Handler, allowedHost string) *SingleHost {
- return &SingleHost{handler: handler, allowedHost: allowedHost}
- }
處理請求
現(xiàn)在才是實際的邏輯。為了實現(xiàn)http.Handler,我們的類型秩序?qū)崿F(xiàn)一個方法:
- type Handler interface {
- ServeHTTP(ResponseWriter, *Request)
- }
這就是我們實現(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ù)僅僅檢查請求中的Host頭:
- 如果Host頭匹配初始化函數(shù)設(shè)置的allowedHost ,就調(diào)用封裝handler的ServeHTTP方法。
- 如果Host頭不匹配,就返回403狀態(tài)碼(禁止訪問)。
在后一種情況中,封裝handler的ServeHTTP方法根本就不會被調(diào)用。因此封裝的handler根本不會有任何輸出,實際上它根本就不知道有這樣一個請求到來。
現(xiàn)在我們已經(jīng)完成了自己的中間件,來把它放到應(yīng)用中。這次我們不把Handler直接放到net/http服務(wù)中,而是先把Handler封裝到中間件中。
- singleHosted = NewSingleHost(myHandler, "example.com")
- http.ListenAndServe(":8080", singleHosted)
另外一種方法
我們剛才寫的中間件實在是太簡單了,只有僅僅15行代碼。為了寫這樣的中間件,引入了一個不太通用的方法。由于Go支持函數(shù)第一型和閉包,并且擁有簡潔的http.HandlerFunc包裝器,我們可以將其實現(xiàn)為一個簡單的函數(shù),而不是寫一個單獨的類型。下面是基于函數(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#
這里我們聲明了一個叫做SingleHost的簡單函數(shù),接受一個Handler和允許的主機名。在函數(shù)內(nèi)部,我們創(chuàng)建了一個類似之前版本ServeHTTP的函數(shù)。這個內(nèi)部函數(shù)其實是一個閉包,所以它可以從SingleHost外部訪問。最終,我們通過HandlerFunc把這個函數(shù)用作http.Handler。
使用Handler還是定義一個http.Handler類型完全取決于你。對簡單的情況而已,一個函數(shù)就足夠了。但是隨著中間件功能的復(fù)雜,你應(yīng)該考慮定義自己的數(shù)據(jù)結(jié)構(gòu),把邏輯獨立到多個方法中。
實際上,標(biāo)準(zhǔn)庫這兩種方法都用了。StripPrefix 是一個返回HandlerFunc的函數(shù)。雖然TimeoutHandler也是一個函數(shù),但它返回了處理請求的自定義的類型。
更復(fù)雜的情況
我們的SingleHost中間件非常簡單:先檢查請求的一個屬性,然后要么什么也不管,把請求直接傳給封裝的Handler;要么自己返回一個響應(yīng),根本不讓封裝的Handler處理這次請求。然而,有些情況是這樣的,不但基于請求觸發(fā)一些動作,還要在封裝的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.
問題是
做其他的響應(yīng)內(nèi)容操作比較麻煩。比如,如果我們想在響應(yīng)內(nèi)容前寫入一些數(shù)據(jù)。如果我們在封裝的handler前調(diào)用Write(),那么封裝的handler就好失去對HTTP狀態(tài)碼和HTTP頭的控制。因為第一次調(diào)用Write()會直接將頭輸出。
想要修改原有輸出(比如,替換其中的某些字符串),改變特定的HTTP頭,設(shè)置不同的狀態(tài)碼也都因為同樣的原因而不可行:當(dāng)封裝的handler返回時,上述數(shù)據(jù)早已被發(fā)送給客戶端了。
為了處理這樣的需求,我們需要一種特殊的可以用做buffer的ResponseWriter,它能夠收集、暫存輸出以用于修改等操作,最后再發(fā)送給客戶端。我們可以將這個帶buffer的ResponseWriter傳給封裝的handler,而不是真實的RW,這樣就避免直接發(fā)送數(shù)據(jù)給客戶端。
幸運的是,在Go標(biāo)準(zhǔn)庫中確實存在這樣一個工具。net/http/httptest中的ResponseRecorder就是這樣的:它保存狀態(tài)碼,一個保存響應(yīng)頭的字典,將輸出累計在buffer中。盡管是用于測試(這個包名暗示了這一點),它還是很好的滿足了我們的需求。
讓我們看一個使用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的輸出。如果不用我們的中間件封裝,原來的handler僅僅會輸出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需要給用戶提供一種獲取CSRF 密鑰的方式以及錯誤原因(如果有錯誤的話)。
對這種需求,一個合適的模型就是使用一個隱藏的map,將http.Request指針指向需要的數(shù)據(jù),然后暴露一個包級別(handler級別)的函數(shù)來訪問這些數(shù)據(jù)。
我在nosurf中也使用了這種模型。這里,我創(chuàng)建了一個全局的上下文map。注意到,由于默認(rèn)情況下Go的map并不是并發(fā)訪問安全的,需要一個mutex。
- type csrfContext struct {
- token string
- reason error
- }
- var (
- contextMap = make(map[*http.Request]*csrfContext)
- cmMutex = new(sync.RWMutex)
- )
使用handler設(shè)置數(shù)據(jù),然后通過暴露的函數(shù)Token()來獲取數(shù)據(jù)。
- func Token(req *http.Request) string {
- cmMutex.RLock()
- defer cmMutex.RUnlock()
- ctx, ok := contextMap[req]
- if !ok {
- return ""
- }
- return ctx.token
- }
你可以在nosurf的代碼庫context.go中找到完整的實現(xiàn)。
雖然我選擇在nosurf中自己實現(xiàn)這種需求,但實際上存在一個handygorilla/context包,它實現(xiàn)了一個通用的保存請求信息的map。在大多數(shù)情況下,這個包足以滿足你的需求,避免你在自己實現(xiàn)一個共享存儲時踩坑。它甚至還有一個自己的中間件能在請求處理結(jié)束之后清除請求信息。
總結(jié)
這篇文章的目的是吸引Go用戶對中間件概念的注意以及展示使用Go寫中間件的一些基本組件。盡管Go是一個相對年輕的開發(fā)語言,Go擁有非常漂亮的標(biāo)準(zhǔn)HTTP接口。這也是用Go寫中間件是個非常簡單甚至快樂的過程的原因之一。
然而,目前Go仍然缺乏高質(zhì)量的HTTP工具。我之前提到的Go中間件想法,大多都還沒實現(xiàn)。現(xiàn)在你已經(jīng)知道如何用Go寫中間件了,為什么不自己做一個呢?
PS,你可以在一個GitHub gist中找到這篇文章中所有的中間件例子。
當(dāng)前標(biāo)題:用Go語言寫HTTP中間件
當(dāng)前URL:http://www.dlmjj.cn/article/djijigi.html


咨詢
建站咨詢
