# 第 5 章 Gin 中间件 > 学完本章你将:理解中间件洋葱模型、编写自定义中间件、掌握 Recovery/Logger/CORS/鉴权等常用中间件。 ## 5.1 中间件原理 中间件是 `func(*gin.Context)`,通过 `c.Next()` 控制调用链,形成洋葱模型: ``` 请求 -> MW1 -> MW2 -> Handler -> MW2 -> MW1 -> 响应 ``` ```go 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 注册级别 ```go // 全局 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` 源码精读: ```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`: ```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`: ```yaml cors: allow_origins: ["*"] allow_methods: ["GET","POST","PUT","DELETE","OPTIONS"] ``` 生产建议收紧为具体域名,避免 `*` + `Allow-Credentials: true` 的组合(浏览器会拦截)。 ## 5.6 自定义鉴权中间件示例 ```go 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(扩展) ```go // 请求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 `,失败返回 401。 3. 调整本项目 `router.New` 中间件顺序,观察 panic 时日志差异。