StreamBox/docs/05-gin-中间件.md

4.3 KiB
Raw Blame History

第 5 章 Gin 中间件

学完本章你将:理解中间件洋葱模型、编写自定义中间件、掌握 Recovery/Logger/CORS/鉴权等常用中间件。

5.1 中间件原理

中间件是 func(*gin.Context),通过 c.Next() 控制调用链,形成洋葱模型:

请求 -> MW1 -> MW2 -> Handler -> MW2 -> MW1 -> 响应
func MW1() gin.HandlerFunc {
    return func(c *gin.Context) {
        fmt.Println("MW1 before")
        c.Next() // 调用后续
        fmt.Println("MW1 after")
    }
}
r.Use(MW1(), MW2())

若不调用 c.Next() 或调用 c.Abort(),则中断后续。c.AbortWithStatusJSON 直接返回响应。

5.2 注册级别

// 全局
r.Use(middleware.ZapLogger())

// 分组
v1 := r.Group("/api/v1", authMiddleware())
authed := r.Group("/admin")
authed.Use(authMiddleware())

// 单路由
r.GET("/secret", authMiddleware(), secretHandler)

本项目 router.New 中三件套全局注册:Recovery、ZapLogger、Cors。

5.3 Recovery 恢复 panic

internal/middleware/recovery.go 源码精读:

func Recovery() gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            if r := recover(); r != nil {
                logger.Log.Error("panic recovered", ...)
                c.AbortWithStatusJSON(500, gin.H{"code":500,"msg":"internal server error"})
            }
        }()
        c.Next()
    }
}
  • 防止单请求 panic 导致进程崩溃
  • 记录错误日志并返回 500
  • 业务代码中不要滥用 panic

5.4 ZapLogger 请求日志

internal/middleware/logger.go:

func ZapLogger() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        path := c.Request.URL.Path
        c.Next()
        latency := time.Since(start)
        logger.Log.Info("request",
            zap.String("method", c.Request.Method),
            zap.String("path", path),
            zap.Int("status", c.Writer.Status()),
            zap.Duration("latency", latency),
        )
    }
}

记录方法、路径、状态码、延迟、IP、User-Agent。生产可在此追加 traceID。

5.5 CORS 跨域

internal/middleware/cors.go 核心逻辑:

  • 根据 config.CorsConfig.AllowOrigins 判断是否允许 Origin
  • 设置 Access-Control-Allow-Origin / Methods / Headers
  • OPTIONS 预检请求直接 204 返回

配置见 config/config.yaml:

cors:
  allow_origins: ["*"]
  allow_methods: ["GET","POST","PUT","DELETE","OPTIONS"]

生产建议收紧为具体域名,避免 * + Allow-Credentials: true 的组合(浏览器会拦截)。

5.6 自定义鉴权中间件示例

func Auth() gin.HandlerFunc {
    return func(c *gin.Context) {
        token := c.GetHeader("Authorization")
        if token == "" {
            c.AbortWithStatusJSON(401, gin.H{"code": 401, "msg": "unauthorized"})
            return
        }
        // 校验 token,解析用户
        c.Set("userID", 123) // 传递给后续 handler
        c.Next()
    }
}

// handler 中获取
func Me(c *gin.Context) {
    uid, _ := c.Get("userID")
    c.JSON(200, gin.H{"userID": uid})
}

本项目已引入 github.com/golang-jwt/jwt/v5 与 casbin,可在鉴权 middleware 中集成 JWT 校验 + Casbin 鉴权。

5.7 限流/超时/请求ID(扩展)

// 请求ID
func RequestID() gin.HandlerFunc {
    return func(c *gin.Context) {
        id := c.GetHeader("X-Request-ID")
        if id == "" { id = uuid.NewString() }
        c.Set("requestID", id)
        c.Header("X-Request-ID", id)
        c.Next()
    }
}

// 超时
func Timeout(d time.Duration) gin.HandlerFunc {
    return func(c *gin.Context) {
        ctx, cancel := context.WithTimeout(c.Request.Context(), d)
        defer cancel()
        c.Request = c.Request.WithContext(ctx)
        c.Next()
    }
}

5.8 常见坑

  • 中间件顺序重要:Recovery 应最先注册,保证捕获所有后续 panic
  • c.Abort() 后仍会执行已进入的中间件的 c.Next() 之后代码,需注意逻辑
  • 不要在中间件中做过重同步操作,会阻塞所有请求

5.9 动手练习

  1. 编写 RequestID 中间件并在日志中打印。
  2. 编写 Auth 中间件,校验 Authorization: Bearer <token>,失败返回 401。
  3. 调整本项目 router.New 中间件顺序,观察 panic 时日志差异。