GORM的BeforeQuery钩子是拦截慢SQL分页的关键防线,因中间件无法感知SQL构造过程;须在钩子中校验OFFSET值、ORDER BY存在性、排序字段白名单、Raw语句及游标分页合规性,并联动索引策略。

中间件拦不住慢SQL分页,GORM钩子才是关键防线。 因为慢SQL发生在数据库执行前的SQL构造阶段,而HTTP中间件只看到请求参数,根本不知道你后面会生成OFFSET 100000还是WHERE 1=1——它连SQL长什么样都不知道。
为什么GORM分页参数校验不能放在gin中间件里
中间件能拿到page和size,但无法关联到具体哪条GORM查询;更危险的是,业务代码可能绕过统一分页封装,直接写db.Offset(x).Limit(y)。常见误用包括:
- 中间件校验了
page=1000,但没拦住db.Where("status = ?", status).Order("id DESC").Offset(99999).Limit(20) - 前端传
size=999999,中间件截断成100,可业务层又手动拼了Raw("SELECT * FROM users LIMIT ? OFFSET ?", 999999, 0) - 分页逻辑分散在多个handler里,中间件校验规则不一致(有的允0页,有的不允),导致防护漏出
GORM BeforeQuery 钩子里怎么拦非法分页
必须在SQL真正发往数据库前做拦截,BeforeQuery是唯一可靠位置。重点不是“有没有分页”,而是“分页是否安全”:
- 检查
stmt.SQL.String()是否含OFFSET且数值过大(如>100000),直接return errors.New("offset too large") - 检查
stmt.Clauses里是否有ORDER BY,没有就拒绝(避免无序分页导致数据错乱) - 对
Raw语句单独处理:正则匹配OFFSET\s+(\d+),提取数值后判断 - 白名单控制排序字段:
if !slices.Contains([]string{"id", "created_at", "updated_at"}, orderByField) { return errors.New("invalid order field") }
游标分页替代方案必须在GORM层强制落地
靠文档或约定推不动,得让错的写法跑不起来。在BeforeQuery里识别传统分页模式并报错,同时提供标准游标接口:
- 检测到
OFFSET且无WHERE id > ?类条件时,返回明确错误:"use cursor pagination: WHERE id > ? ORDER BY id ASC" - 允许注册游标字段白名单:
cursorFields := map[string]bool{"id": true, "created_at": true},不在其中的字段禁止用于游标条件 - 对
last_id参数做类型强校验:若传入字符串"abc",strconv.ParseInt失败就直接中断,不尝试默认值或静默转换 - 游标查询必须带
Order,且顺序需与游标条件方向一致(WHERE id > ?→Order("id ASC"))
最易被忽略的一点:分页拦截必须和索引策略联动。光拦OFFSET没用,如果WHERE created_at > ? ORDER BY created_at DESC没走索引,照样慢。钩子里可以打日志告警,但索引得DBA配合建——别指望GORM自动解决物理扫描问题。











