简介

开发 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.HandlerFuncfunc(*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 框架都会实现.

当前部分的代码

作为版本 v0.8.0


帅气猫咪
105 声望13 粉丝