自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

用Go語(yǔ)言寫(xiě)HTTP中間件

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

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)作。

我最近的Gonosurf 也是一個(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ù)從特定的域(在HTTPHost頭中有域信息)來(lái)訪問(wèn)服務(wù)器。這樣的中間件可以保護(hù)應(yīng)用程序不受“主機(jī)欺騙攻擊

定義類(lèi)型

為了方便,讓我們?yōu)檫@個(gè)中間件定義一種類(lèi)型,叫做SingleHost。

  1. type SingleHost struct { 
  2.   
  3.     handler     http.Handler 
  4.   
  5.     allowedHost string 
  6.   

只包含兩個(gè)字段:

  • 封裝的Handler。如果是有效的Host訪問(wèn),我們就調(diào)用這個(gè)Handler。
  • 允許的主機(jī)值。

由于我們把字段名小寫(xiě)了,使得該字段只對(duì)我們自己的包可見(jiàn)。我們還應(yīng)該寫(xiě)一個(gè)初始化函數(shù)。

  1. func NewSingleHost(handler http.Handler, allowedHost string) *SingleHost { 
  2.   
  3.     return &SingleHost{handler: handler, allowedHost: allowedHost} 
  4.   

處理請(qǐng)求

現(xiàn)在才是實(shí)際的邏輯。為了實(shí)現(xiàn)http.Handler,我們的類(lèi)型秩序?qū)崿F(xiàn)一個(gè)方法:

  1. type Handler interface { 
  2.   
  3.         ServeHTTP(ResponseWriter, *Request) 
  4.   

這就是我們實(shí)現(xiàn)的方法:

  1. func (s *SingleHost) ServeHTTP(w http.ResponseWriter, r *http.Request) { 
  2.   
  3.     host :r.Host 
  4.   
  5.     if host == s.allowedHost { 
  6.   
  7.         s.handler.ServeHTTP(w, r) 
  8.   
  9.     } else { 
  10.   
  11.         w.WriteHeader(403) 
  12.   
  13.     } 
  14.   

ServeHTTP 函數(shù)僅僅檢查請(qǐng)求中的Host頭:

  • 如果Host頭匹配初始化函數(shù)設(shè)置的allowedHost ,就調(diào)用封裝handlerServeHTTP方法。
  • 如果Host頭不匹配,就返回403狀態(tài)碼(禁止訪問(wèn))。

在后一種情況中,封裝handlerServeHTTP方法根本就不會(huì)被調(diào)用。因此封裝的handler根本不會(huì)有任何輸出,實(shí)際上它根本就不知道有這樣一個(gè)請(qǐng)求到來(lái)。

現(xiàn)在我們已經(jīng)完成了自己的中間件,來(lái)把它放到應(yīng)用中。這次我們不把Handler直接放到net/http服務(wù)中,而是先把Handler封裝到中間件中。

  1. singleHosted = NewSingleHost(myHandler, "example.com") 
  2.   
  3. 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ù)的中間件版本。

  1. func SingleHost(handler http.Handler, allowedHost string) http.Handler { 
  2.   
  3.     ourFunc :func(w http.ResponseWriter, r *http.Request) { 
  4.   
  5.         host :r.Host 
  6.   
  7.         if host == allowedHost { 
  8.   
  9.             handler.ServeHTTP(w, r) 
  10.   
  11.         } else { 
  12.   
  13.             w.WriteHeader(403) 
  14.   
  15.         } 
  16.   
  17.     } 
  18.   
  19.     return http.HandlerFunc(ourFunc) 
  20.   

#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()即可:

  1. type AppendMiddleware struct { 
  2.     handler http.Handler 
  3.   
  4. func (a *AppendMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { 
  5.     a.handler.ServeHTTP(w, r) 
  6.     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ù)端了。

為了處理這樣的需求,我們需要一種特殊的可以用做bufferResponseWriter,它能夠收集、暫存輸出以用于修改等操作,最后再發(fā)送給客戶(hù)端。我們可以將這個(gè)帶bufferResponseWriter傳給封裝的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)容的所有東西,是為了更完整的演示。

  1. type ModifierMiddleware struct { 
  2.   
  3.     handler http.Handler 
  4.   
  5.   
  6. func (m *ModifierMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { 
  7.   
  8.     rec :httptest.NewRecorder() 
  9.   
  10.     // passing a ResponseRecorder instead of the original RW 
  11.   
  12.     m.handler.ServeHTTP(rec, r) 
  13.   
  14.     // after this finishes, we have the response recorded 
  15.   
  16.     // and can modify it before copying it to the original RW 
  17.   
  18.     // we copy the original headers first 
  19.   
  20.     for k, v :range rec.Header() { 
  21.   
  22.         w.Header()[k] = v 
  23.   
  24.     } 
  25.   
  26.     // and set an additional one 
  27.   
  28.     w.Header().Set("X-We-Modified-This", "Yup") 
  29.   
  30.     // only then the status code, as this call writes out the headers 
  31.   
  32.     w.WriteHeader(418) 
  33.   
  34.     // the body hasn't been written (to the real RW) yet, 
  35.   
  36.     // so we can prepend some data. 
  37.   
  38.     w.Write([]byte("Middleware says hello again. ")) 
  39.   
  40.     // then write out the original body 
  41.   
  42.     w.Write(rec.Body.Bytes()) 
  43.   

下面是我們包裝的handler的輸出。如果不用我們的中間件封裝,原來(lái)的handler僅僅會(huì)輸出Success!。

  1. HTTP/1.1 418 I'm a teapot 
  2.   
  3. X-We-Modified-This: Yup 
  4.   
  5. Content-Type: text/plain; charset=utf-8 
  6.   
  7. Content-Length: 37 
  8.   
  9. Date: Tue, 03 Sep 2013 18:41:39 GMT 
  10.   
  11. 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)情況下Gomap不是并發(fā)訪問(wèn)安全的,需要一個(gè)mutex。

  1. type csrfContext struct { 
  2.   
  3.     token string 
  4.   
  5.     reason error 
  6.   
  7.   
  8. var ( 
  9.   
  10.     contextMap = make(map[*http.Request]*csrfContext) 
  11.   
  12.     cmMutex    = new(sync.RWMutex) 
  13.   

使用handler設(shè)置數(shù)據(jù),然后通過(guò)暴露的函數(shù)Token()來(lái)獲取數(shù)據(jù)。

  1. func Token(req *http.Request) string { 
  2.   
  3.     cmMutex.RLock() 
  4.   
  5.     defer cmMutex.RUnlock() 
  6.   
  7.     ctx, ok :contextMap[req] 
  8.   
  9.     if !ok { 
  10.   
  11.             return "" 
  12.   
  13.     } 
  14.   
  15.     return ctx.token 
  16.   

你可以在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/

 

責(zé)任編輯:陳四芳 來(lái)源: 伯樂(lè)在線
相關(guān)推薦

2015-12-21 14:56:12

Go語(yǔ)言Http網(wǎng)絡(luò)協(xié)議

2021-10-06 19:03:35

Go中間件Middleware

2022-11-18 07:54:02

Go中間件項(xiàng)目

2021-02-11 08:21:02

中間件開(kāi)發(fā)CRUD

2011-05-24 15:10:48

2020-06-28 09:20:33

代碼開(kāi)發(fā)Go

2018-07-29 12:27:30

云中間件云計(jì)算API

2018-02-01 10:19:22

中間件服務(wù)器系統(tǒng)

2016-11-11 21:00:46

中間件

2024-05-06 12:30:51

Go語(yǔ)言中間件

2023-06-29 10:10:06

Rocket MQ消息中間件

2023-10-24 07:50:18

消息中間件MQ

2009-06-16 15:55:06

JBoss企業(yè)中間件

2012-11-30 10:21:46

移動(dòng)中間件

2017-12-11 13:30:49

Go語(yǔ)言數(shù)據(jù)庫(kù)中間件

2024-02-06 14:05:00

Go中間件框架

2024-08-09 08:11:02

2012-11-01 15:16:22

金蝶中間件研究院院長(zhǎng)

2021-06-15 10:01:02

應(yīng)用系統(tǒng)軟件

2013-03-14 22:54:15

PaaS中間件平臺(tái)即服務(wù)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)