简介

开发 web 应用的时候, 很多地方都需要使用中间件来统一处理一些任务,

比如记录日志, 登录校验等.

gin 也提供了中间件功能.

gin 的中间件

在项目创建之初, 就已经导入了一些中间件, 当时没有仔细介绍.

g.Use(gin.Logger())

g.Use(gin.Recovery())

g.Use(middleware.NoCache())

g.Use(middleware.Options())

g.Use(middleware.Secure())

前面两个是 gin 自带的中间件, 分别是日志记录和错误恢复.

后面三个是设置一些 header, 具体是阻止缓存响应, 响应 options 请求,

以及浏览器安全设置.

// 阻止缓存响应

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) 的别名.

中间件可以被定义在三个地方

全局中间件

Group 中间件

单个路由中间件

一点需要注意的是在 middleware 和 handler 中使用 goroutine 时,

应该使用 gin.Context 的只读副本, 例如 cCp := context.Copy().

另一点则是注意中间件的顺序.

官方的示例如下:

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 的中间件知识之后, 就可以根据需求使用中间件了.

实现一个中间件在每个请求中设置 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 框架都会实现.

当前部分的代码