使用Go開發HTTP中間件


原文地址

   再web開發的背景下,“中間件”通常意思是“包裝原始應用並添加一些額外的功能的應用的一部分”。這個概念似乎總是不被人理解,但是我認為中間件非常棒。
   首先,一個好的中間件有一個責任就是可插拔並且自足。這就意味着你可以在接口級別嵌入你的中間件他就能直接運行。它不會影響你編碼方式,不是框架,僅僅是你請求處理里面的一層而已。完全沒必要重寫你的代碼,如果你想使用中間件的一個功能,你就幫他插入到那里,如果不想使用了,就可以直接移除。
   縱觀Go語言,中間件是非常普遍的,即使在標准庫中。雖然開始的時候不會那么明顯,在標准庫net/http中的函數StripText或者TimeoutHandler就是我們要定義和的中間件的樣子,處理請求和相應的時候他們包裝你的handler,並處理一些額外的步驟。
   我最近寫的Go包nosurf同樣也是個中間件。我特意將他從頭開始設計。在大多數情況下,你不需要在應用層擔心CSRF攻擊,nosurf像其他的中間件一樣可以自足,並且和net/http的接口無縫銜接。
   同樣你還可以使用中間件做:

  • 隱藏長度防止緩沖攻擊
  • 速度限制
  • 屏蔽爬蟲
  • 提供調試信息
  • 添加HSTS,X-Frame-Options頭
  • 從錯誤中恢復
  • 等等
編寫一個簡單的中間件

   我們的第一個例子是寫一個只允許一個域名下的用戶訪問的中間件,通過HTTP的HOSTheader實現。這樣的中間件可以防止主機欺騙攻擊

類型的機構

   首先我們定義一個結構體,叫做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)
)
數據由Token設置:
func Token(req *http.Request) string {
    cmMutex.RLock()
    defer cmMutex.RUnlock()

    ctx, ok := contextMap[req]
    if !ok {
            return ""
    }

    return ctx.token
}
源碼可以再nosurf的項目的 context.go中找到。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM