简介
开发 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 框架都会实现.
当前部分的代码