167 lines
4.3 KiB
Markdown
167 lines
4.3 KiB
Markdown
# 第 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 <token>`,失败返回 401。
|
||
3. 调整本项目 `router.New` 中间件顺序,观察 panic 时日志差异。
|