在写接口的时候,有很多通用逻辑(比如日志记录、Token 鉴权、参数校验)都需要放在业务处理函数之前执行,每个接口都写一遍太冗余了。有没有一种方法能把这些前置通用逻辑整合起来,统一管
理和复用,还不用侵入业务代码?
这时候就凸现了中间件的作用
一、核心概念:路由中间件是什么?
路由中间件是介于 HTTP 请求与路由业务逻辑之间的通用处理函数,它可以拦截、预处理、后处理请求 / 响应,支持链式调用。
本质 :对路由功能的扩展,遵循 "单一职责" 原则,一个中间件只处理一类通用逻辑。
执行流程 :客户端请求 → 中间件1 → 中间件2 → ... → 路由业务函数 → 中间件2 → 中间件1 → 客户端响应
常用类型:日志、跨域、鉴权、参数校验、限流。
二、为什么需要路由中间件?
避免重复代码 :把日志、鉴权等通用逻辑抽离成中间件,无需在每个路由函数中重复编写。
统一逻辑管控 :比如全局拦截未授权请求、统一记录接口访问日志,提升代码可维护性。
灵活扩展功能 :支持按需注册中间件(全局 / 单个路由 / 路由组),不侵入业务代码。
提升接口安全性:通过参数校验、限流等中间件,过滤非法请求,防止服务被攻击。
三、怎么用?5 类常用中间件代码示例(Go Gin 框架)
以下示例基于 Go Gin 框架(新手易上手),先初始化基础项目:
go
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"time"
"sync"
)
func main() {
r := gin.Default() // 默认包含 Logger + Recovery 中间件
// 注册中间件 + 路由定义
// ... 后续中间件注册代码 ...
_ = r.Run(":8080") // 启动服务
}
- 日志中间件
功能:记录请求的方法、路径、耗时、状态码。
go
// 自定义日志中间件
func LoggerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 1. 请求前:记录开始时间 + 请求信息
startTime := time.Now()
path := c.Request.URL.Path
method := c.Request.Method
// 2. 执行后续中间件/路由函数
c.Next()
// 3. 请求后:记录响应信息
latency := time.Since(startTime)
statusCode := c.Writer.Status()
clientIP := c.ClientIP()
fmt.Printf("[%s] %s | %s | %s | %d | %v\n",
time.Now().Format("2006-01-02 15:04:05"),
clientIP, method, path, statusCode, latency)
}
}
// 注册方式1:全局生效(所有路由)
r.Use(LoggerMiddleware())
// 注册方式2:单个路由生效
r.GET("/hello", LoggerMiddleware(), func(c *gin.Context) {
c.JSON(200, gin.H{"message": "hello world"})
})
- 跨域中间件
功能:解决前后端分离项目的跨域请求限制(浏览器同源策略)。
go
func CorsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 允许所有源(生产环境需指定具体域名)
origin := c.Request.Header.Get("Origin")
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
// 允许的请求头
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
// 允许的请求方法
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
// 允许携带Cookie
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
// 预检请求(OPTIONS)直接返回200
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}
// 全局注册(前后端分离必加)
r.Use(CorsMiddleware())
- 鉴权中间件
功能:校验请求头中的 Token,拦截未授权访问。
go
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 从请求头获取Token
token := c.Request.Header.Get("Authorization")
if token == "" || token != "valid_token_123" { // 实际需结合JWT/数据库校验
c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权或Token无效"})
c.Abort() // 终止请求,不执行后续逻辑
return
}
// 验证通过,将用户信息存入上下文(供后续路由使用)
c.Set("userID", 1001)
c.Next()
}
}
// 注册方式3:路由组生效(比如需要鉴权的用户接口)
userGroup := r.Group("/user", AuthMiddleware())
{
userGroup.GET("/info", func(c *gin.Context) {
userID := c.GetInt("userID")
c.JSON(200, gin.H{"userID": userID, "username": "test_user"})
})
}
- 参数校验中间件
功能:验证请求参数(比如必填字段、格式是否合法)。
go
func ValidateMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 示例:校验GET请求的id参数
id := c.Query("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "id参数不能为空"})
c.Abort()
return
}
// 进一步校验id是否为数字(简化示例)
for _, ch := range id {
if ch < '0' || ch > '9' {
c.JSON(http.StatusBadRequest, gin.H{"error": "id必须为数字"})
c.Abort()
return
}
}
c.Next()
}
}
// 单个路由使用
r.GET("/item", ValidateMiddleware(), func(c *gin.Context) {
id := c.Query("id")
c.JSON(200, gin.H{"itemID": id, "name": "test_item"})
})
- 限流中间件(固定窗口限流)
功能:限制单位时间内的请求次数,防止接口被刷。
go
// 限流结构体
type RateLimiter struct {
mu sync.Mutex
count map[string]int // 记录每个IP的请求次数
limit int // 单位时间内最大请求数
window time.Duration // 时间窗口
lastReset time.Time // 上次重置时间
}
// 初始化限流中间件
func NewRateLimiter(limit int, window time.Duration) *RateLimiter {
return &RateLimiter{
count: make(map[string]int),
limit: limit,
window: window,
lastReset: time.Now(),
}
}
func (rl *RateLimiter) Middleware() gin.HandlerFunc {
return func(c *gin.Context) {
rl.mu.Lock()
defer rl.mu.Unlock()
// 重置时间窗口
if time.Since(rl.lastReset) > rl.window {
rl.count = make(map[string]int)
rl.lastReset = time.Now()
}
// 获取客户端IP
ip := c.ClientIP()
if rl.count[ip] >= rl.limit {
c.JSON(http.StatusTooManyRequests, gin.H{"error": "请求过于频繁,请稍后再试"})
c.Abort()
return
}
rl.count[ip]++
c.Next()
}
}
// 使用:10秒内每个IP最多5次请求
limiter := NewRateLimiter(5, 10*time.Second)
r.Use(limiter.Middleware())
四、使用中间件会出现什么问题?
- 中间件执行顺序混乱
现象:比如先执行鉴权中间件,再执行日志中间件,导致未授权请求的日志没有被记录;或者跨域中间件注册在鉴权之后,导致预检请求被拦截。
原因:中间件的注册顺序决定了执行顺序(先注册先执行),响应阶段则相反(后注册先执行)。 - 忘记调用 c.Next() 或误用 c.Abort()
现象 1:调用 c.Abort() 后没有 return,导致后续逻辑继续执行;
现象 2:没有调用 c.Next(),导致路由业务函数无法执行。
原因:不熟悉 Gin 中间件的执行机制。 - 全局中间件过度使用
现象:把只需要给部分路由使用的中间件(比如限流)注册为全局,导致不必要的性能损耗。
原因:对中间件的作用范围(全局 / 路由组 / 单个路由)理解不清。 - 中间件逻辑臃肿
现象:一个中间件同时处理日志、鉴权、参数校验多个逻辑,导致代码难以维护和调试。
原因:违反 "单一职责" 原则。 - 并发安全问题
现象:比如限流中间件中的 count 字典,在高并发下出现数据竞争,导致限流失效。
原因:没有对共享变量加锁。
五、如何解决这些问题?
- 规范中间件执行顺序
遵循 "基础功能优先,业务功能后置" 的原则,推荐注册顺序:
plaintext
跨域中间件 → 日志中间件 → 限流中间件 → 鉴权中间件 → 参数校验中间件 → 业务路由
原因:跨域预检请求需要最先处理,日志需要记录所有请求(包括未授权的),限流要在鉴权前防止恶意请求刷鉴权接口。 - 正确使用 c.Next() 和 c.Abort()
c.Next():必须调用,用于执行后续中间件 / 路由函数,通常放在中间件逻辑的中间位置。
c.Abort():用于终止请求,调用后必须加 return,否则后续代码会继续执行。
正确示例:
go
运行
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if tokenInvalid {
c.JSON(401, gin.H{"error": "未授权"})
c.Abort()
return // 必须return,防止后续逻辑执行
}
c.Next() // 验证通过,执行后续逻辑
}
}
- 合理选择中间件作用范围
表格
作用范围 适用场景 注册方式
全局 跨域、日志、全局限流 r.Use(middleware)
路由组 同一模块的鉴权、参数校验 group := r.Group("/user", middleware)
单个路由 特殊接口的自定义校验 r.GET("/item", middleware, handler) - 坚持 "单一职责" 原则
一个中间件只做一件事:
错误示例:一个中间件同时处理日志 + 鉴权;
正确示例:拆分为 LoggerMiddleware 和 AuthMiddleware,分别注册。 - 解决并发安全问题
对中间件中的共享变量加锁,比如限流中间件中的 sync.Mutex,或者使用并发安全的数据结构(如 sync.Map):
go
// 改进后的限流中间件计数(使用sync.Map)
type RateLimiter struct {
count sync.Map // 并发安全的map
// ... 其他字段 ...
}
六、总结
路由中间件是提升路由开发效率和接口安全性的核心工具,新手使用时需牢记:单一职责、顺序规范、作用范围清晰、并发安全。通过本文的 5 类常用中间件示例,可快速在项目中落地,避免重复造轮子。