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

167 lines
4.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 第 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 时日志差异。