開發 web 應用的時候, 不少地方都須要使用中間件來統一處理一些任務, 好比記錄日誌, 登陸校驗等.git
gin 也提供了中間件功能.github
在項目建立之初, 就已經導入了一些中間件, 當時沒有仔細介紹.web
g.Use(gin.Logger())
g.Use(gin.Recovery())
g.Use(middleware.NoCache())
g.Use(middleware.Options())
g.Use(middleware.Secure())
複製代碼
前面兩個是 gin 自帶的中間件, 分別是日誌記錄和錯誤恢復. 後面三個是設置一些 header, 具體是阻止緩存響應, 響應 options 請求, 以及瀏覽器安全設置.json
// 阻止緩存響應
func NoCache() gin.HandlerFunc {
return func(ctx *gin.Context) {
ctx.Header("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate, value")
ctx.Header("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
ctx.Header("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
ctx.Next()
}
}
// 響應 options 請求, 並退出
func Options() gin.HandlerFunc {
return func(ctx *gin.Context) {
if ctx.Request.Method != "OPTIONS" {
ctx.Next()
} else {
ctx.Header("Access-Control-Allow-Origin", "*")
ctx.Header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
ctx.Header("Access-Control-Allow-Headers", "authorization, origin, content-type, accept")
ctx.Header("Allow", "HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS")
ctx.Header("Content-Type", "application/json")
ctx.AbortWithStatus(200)
}
}
}
// 安全設置
func Secure() gin.HandlerFunc {
return func(ctx *gin.Context) {
ctx.Header("Access-Control-Allow-Origin", "*")
ctx.Header("X-Frame-Options", "DENY")
ctx.Header("X-Content-Type-Options", "nosniff")
ctx.Header("X-XSS-Protection", "1; mode=block")
if ctx.Request.TLS != nil {
ctx.Header("Strict-Transport-Security", "max-age=31536000")
}
// Also consider adding Content-Security-Policy headers
// ctx.Header("Content-Security-Policy", "script-src 'self' https://cdnjs.cloudflare.com")
}
}
複製代碼
gin 的中間件結構就是一個返回 func(ctx *gin.Context)
的函數, 又叫作 gin.HandlerFunc
. 本質上和普通的 handler 沒什麼不一樣, gin.HandlerFunc
是func(*Context)
的別名.瀏覽器
中間件能夠被定義在三個地方緩存
一點須要注意的是在 middleware 和 handler 中使用 goroutine 時, 應該使用 gin.Context 的只讀副本, 例如 cCp := context.Copy()
.安全
另外一點則是注意中間件的順序.app
官方的示例以下:框架
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
t := time.Now()
// Set example variable
c.Set("example", "12345")
// before request
c.Next()
// after request
latency := time.Since(t)
log.Print(latency)
// access the status we are sending
status := c.Writer.Status()
log.Println(status)
}
}
複製代碼
介紹了 gin 的中間件知識以後, 就能夠根據需求使用中間件了.ide
實現一箇中間件在每一個請求中設置 X-Request-Id
頭.
// 在請求頭中設置 X-Request-Id
func RequestId() gin.HandlerFunc {
return func(ctx *gin.Context) {
requestId := ctx.Request.Header.Get("X-Request-Id")
if requestId == "" {
requestId = uuid.NewV4().String()
}
ctx.Set("X-Request-Id", requestId)
ctx.Header("X-Request-Id", requestId)
ctx.Next()
}
}
複製代碼
設置 header 的同時保存在 context 內部, 經過設置惟一的 ID 以後, 就能夠追蹤一系列的請求了.
再來實現一個日誌記錄的中間件, 雖然 gin 已經自帶了日誌記錄的中間件, 但本身實現能夠更加個性化.
// 定義日誌組件, 記錄每個請求
func Logging() gin.HandlerFunc {
return func(ctx *gin.Context) {
path := ctx.Request.URL.Path
method := ctx.Request.Method
ip := ctx.ClientIP()
// 只記錄特定的路由
reg := regexp.MustCompile("(/v1/user|/login)")
if !reg.MatchString(path) {
return
}
var bodyBytes []byte
if ctx.Request.Body != nil {
bodyBytes, _ = ioutil.ReadAll(ctx.Request.Body)
}
// 讀取後寫回
ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
blw := &bodyLogWriter{
body: bytes.NewBufferString(""),
ResponseWriter: ctx.Writer,
}
ctx.Writer = blw
start := time.Now()
ctx.Next()
// 計算延遲, 和 gin.Logger 的差距有點大
// 這是由於 middleware 相似棧, 先進後出, ctx.Next() 是轉折點
// 因此 gin.Logger 放在最前, 記錄總時長
// Logging 放在最後, 記錄實際運行的時間, 不包含其餘中間件的耗時
end := time.Now()
latency := end.Sub(start)
code, message := -1, ""
var response handler.Response
if err := json.Unmarshal(blw.body.Bytes(), &response); err != nil {
logrus.Errorf(
"response body 不能被解析爲 model.Response struct, body: `%s`, err: `%v`",
blw.body.Bytes(),
err,
)
code = errno.InternalServerError.Code
message = err.Error()
} else {
code = response.Code
message = response.Message
}
logrus.WithFields(logrus.Fields{
"latency": fmt.Sprintf("%s", latency),
"ip": ip,
"method": method,
"path": path,
"code": code,
"message": message,
}).Info("記錄請求")
}
}
複製代碼
在註冊中間件的時候, 將 Logging
放在全局中間件的最後, 將 gin.Logger() 放在全局中間件的最開始. 經過對比延遲, 你能夠發現, 在 handler 處理比較快時, 中間件在總請求耗時中佔據了很大的比例.
因此, 中間件雖然很是實用, 但須要控制全局中間件的數量.
中間件是很是實用的, 基本上 web 框架都會實現.
做爲版本 v0.8.0