业务层必须显式注入WHERE条件,不能依赖数据库RLS或ORM魔法;Go无内置行级权限,需用gorm.Scope动态注入过滤逻辑,且须校验用户ID有效性、防SQL注入、为Preload和聚合查询单独处理权限。

业务层必须显式注入 WHERE 条件,不能依赖数据库 RLS 或 ORM 魔法。Go 没有内置行级权限机制,所有过滤逻辑得在构建查询前就塞进 SQL,否则数据就裸奔了。
gorm.Scope 动态注入 WHERE 条件
用 Scope 是最常用也最可控的方式,它会在每次调用 Find、First 等方法时自动追加条件,避免漏写。
- 定义 scope 函数时,必须显式检查
user.ID是否有效,空用户或未登录状态要 panic 或返回错误,不能静默跳过 - 关联表(如
orders JOIN users ON orders.user_id = users.id)的过滤不能只靠主表 scope,得额外给Preload加 scope,否则关联数据不被限制 - 软删除字段(如
deleted_at IS NULL)要和权限条件一起拼进 scope,顺序无关,但缺一不可 - 示例:
func UserScope(userID uint) func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { return db.Where("user_id = ?", userID) } }
WHERE 条件拼接时防 SQL 注入的硬要求
所有用户可控参数(包括 URL 查询里的 user_id、tenant_id)必须走占位符,禁止字符串拼接。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 错例:
"WHERE user_id = " + strconv.Itoa(userID)—— 直接给注入开后门 - 对例:
db.Where("user_id = ?", userID)或db.Where("tenant_id IN ?", tenantIDs) - 模糊匹配必须手动加通配符:
db.Where("name LIKE ?", "%"+keyword+"%"),且keyword要strings.TrimSpace并限制长度 ≤ 50 - 枚举类字段(如
status)必须白名单校验:if !slices.Contains([]string{"active", "pending"}, status) { return errInvalidStatus }
聚合查询中行级权限与 GROUP BY 的配合陷阱
当你要统计“每个用户有多少订单”,权限过滤和分组必须在同一个查询里完成,不能先查再 in 内存过滤。
立即学习“go语言免费学习笔记(深入)”;
- WHERE 放行级条件(如
user_id = ?),GROUP BY 放维度字段(如status),HAVING 只能用于聚合结果(如HAVING COUNT(*) > 1) - 错例:
SELECT user_id, COUNT(*) FROM orders GROUP BY user_id HAVING user_id = ?——user_id不在 GROUP BY 列表,MySQL 会报错 - 对例:
SELECT status, COUNT(*) FROM orders WHERE user_id = ? GROUP BY status - 如果要按时间天粒度分组,用
DATE(created_at)而不是created_at::date(PostgreSQL 语法),保持跨库兼容
最容易被忽略的是:scope 只作用于主表,Preload 关联数据时默认不继承;还有就是聚合场景下把非分组字段塞进 HAVING——这不会报错但会返回空,问题极难定位。

















