GORM本身不提供分权限分页查询能力,高安全级别全靠手动在每条SQL中显式注入tenant_id、role、scope三要素;Preload和Count默认不继承tenant_id过滤,因它们生成独立SQL且不透传主查询条件,漏加一次WHERE tenant_id = ?即导致跨租户数据泄露。

直接说结论:GORM 本身不提供“分权限分页查询”能力,所谓高安全级别,全靠你手动把 tenant_id、role、scope 这三要素塞进每一条查询语句里——漏一次,就可能查出别人的数据。
为什么 GORM 的 Preload 和 Count 默认不认 tenant_id
GORM v2+ 的 Preload() 和 Count() 是独立 SQL 构建单元,它们不会继承主查询的 Where 条件或自定义 Scope。比如你写:
db.Where("tenant_id = ?", tenantID).Preload("Orders").Find(&user)实际会发两条 SQL:SELECT * FROM users WHERE tenant_id = ?,再执行 SELECT * FROM orders(没带 WHERE tenant_id = ?)。结果就是用户 A 看到了租户 B 的全部订单。
- 永远不用裸调
db.Preload(),改用封装方法:先定义UserRepo.WithTenant(tenantID),内部对每个关联字段都显式加WHERE tenant_id = ? -
Count()必须和主查询同级过滤:db.Where("tenant_id = ? AND status = ?", tenantID, "active").Model(&Order{}).Count(&count),不能依赖 Scope 透传 - 软删除字段
deleted_at要和tenant_id并列约束:Where("tenant_id = ? AND deleted_at IS NULL", tenantID),否则Unscoped()会绕过租户隔离
分页查慢?不是 GORM 慢,是 LIMIT OFFSET 写错了
大 offset 分页(如 OFFSET 100000)在 MySQL/PostgreSQL 上会触发 Using filesort 或全表扫描,延迟从毫秒飙到秒级。这不是 ORM 层能优化的,得换策略。
- 禁用
Limit(n).Offset(m)做后台列表分页,尤其当数据量 > 10 万行时 - 改用游标分页:
WHERE id > ? ORDER BY id ASC LIMIT 50,前端传上一页最后的id值 - 联合索引必须包含
tenant_id:比如(tenant_id, id)或(tenant_id, created_at),否则游标失效 - 如果业务强制要求页码,至少加
WHERE tenant_id = ?后再OFFSET,避免跨租户扫描
SQL 注入风险最常出现在哪里
不是 db.Where("name = ?", name),而是你忍不住手写的 db.Raw() 和 db.Session().Exec()。只要拼接了用户输入,就等于把数据库钥匙交出去。
- 绝对禁止:
db.Raw("SELECT * FROM users WHERE name = '" + name + "'")——name是"admin' OR '1'='1"就完蛋 - 允许但需严审:
db.Raw("SELECT * FROM users WHERE tenant_id = ? AND status IN ?", tenantID, statusList),第二个参数必须是[]interface{}类型 - 所有动态表名、字段名(比如按租户切分的
logs_tenant_abc)必须白名单校验,不能由前端传入直接拼接 - 日志里打印 SQL 时,确保
logger.Config.ParameterizedQueries = true,否则你看到的全是问号,查不出真实问题
权限校验和分页怎么串起来才不漏
权限不是查完再过滤,而是在生成 SQL 前就决定“能查哪些字段、哪些行、哪些关联”。GORM 不会帮你做这个决策,得你自己写逻辑。
- 不要在
Find()之后用 Go 循环if !canRead(u)过滤——内存爆、性能差、还可能漏掉关联数据 - 把权限规则转成 WHERE 条件:比如角色
editor只能看自己部门,就加WHERE dept_id IN (SELECT dept_id FROM user_depts WHERE user_id = ?) - 分页前先
Count(),但 Count 的 WHERE 必须和 Find 完全一致,否则总数和页数对不上 - 如果用了 Casbin,别只在校验层 enforce,要把
e.GetImplicitPermissionsForUser(...)拿到的资源 pattern 映射成 SQL 的LIKE或IN条件,嵌入查询
最易被忽略的一点:事务内多次查询必须复用同一个 *gorm.DB 实例,并确保每次调用都显式传入 tenant_id。用 db.Session(&gorm.Session{NewDB: true}) 开新 session 会丢失你之前设的所有 Scope 和绑定参数。


















