Casbin+Gin实现RBAC的正确做法是:JWT解析后一次性查出用户所有角色和权限码存入context,sub须与模型g规则角色标识一致,路径用keyMatch3裁剪前缀,HTTP方法需精确匹配,超级管理员应通过策略放行而非跳过中间件,资源所有权校验必须在业务层单独处理。

直接用 Casbin + Gin 实现 RBAC,别自己拼 SQL 查权限树——90% 的越权漏洞都出在手动 JOIN 和 context 覆盖上。
为什么不能在中间件里实时查 DB 加载权限
常见错误是:每次请求都从 c.Get("user_id") 拿 ID,再查 user_roles 和 role_permissions 表,把结果塞进 context。这看似合理,但实际踩了三个坑:
- 并发下多个 goroutine 共享同一个
context实例,A 用户的权限可能被 B 用户的查询覆盖 - 没加锁或没走连接池隔离,DB 查询变成性能瓶颈,QPS 上不去
- 没预加载关联数据,一次请求触发 N+1 查询,日志里全是重复的
SELECT * FROM permissions WHERE id IN (...)
正确做法是:JWT 解析后,一次性查出用户所有角色 + 所有权限码(user:read:own、order:write 这类),转成 map[string]struct{} 存进 context,后续校验走 O(1) 查找。
Enforce(sub, obj, act) 里的 sub 怎么设才不出错
很多人直接传 user.ID 字符串,结果 Casbin 模型里匹配的是 role:1 或 admin,导致永远返回 false。关键点在于:sub 必须和模型中 g 规则定义的角色标识一致。
- 如果你用的是
g, user_123, role_admin这种继承规则,sub就该传"user_123",不是"123" - 如果模型里写的是
g, admin, role:1,那sub应该是"admin",且你要确保数据库里用户字段存的就是这个字符串 - 更稳妥的方式是统一用角色 ID(如
"role_1")作为sub,然后在 JWT payload 里带authority_id或role_code,避免依赖用户名等可变字段
路径匹配用 keyMatch3 还是正则?
Casbin 默认模型里 keyMatch3(r.obj, p.obj) 支持 /api/v1/users/:id 匹配 /api/v1/users/123,但容易漏掉两类场景:
- RESTful 路由带通配符时,比如
/api/v1/orders/*要匹配/api/v1/orders/123/items,得确认keyMatch3是否启用多级通配(默认不支持**) - 前端路由和 API 路由前缀不一致,比如 Vue Router 用
/dashboard,而后端 API 是/api/v1/dashboard,obj值必须做统一裁剪(如用strings.TrimPrefix(path, "/api/v1")) - HTTP 方法校验别只看
GET/POST,要区分PUT /users/123和PATCH /users/123,策略里得写p, role_editor, /users/:id, PUT和p, role_editor, /users/:id, PATCH两条
超级管理员怎么绕过 Casbin 校验
别写 if isSuperAdmin { c.Next(); return } 这种逻辑——它会跳过整个中间件链,导致日志、监控、事务等其他中间件也被跳过。
- 正确做法是在
e.Enforce()前加判断:if user.IsSuper { success = true },保持中间件流程完整 - 或者在 Casbin 策略里加一条全局放行规则:
p, role_super, *, *,再配合模型里的e = some(where (p.eft == allow)),让超级管理员自动命中 - 注意:数据库里
is_super字段必须走索引,别在中间件里做SELECT ... WHERE username = ?再判断,否则每次请求多一次 DB 查询
真正麻烦的不是配置 Casbin,而是把「资源所有权校验」(比如 user_id == post.author_id)和「角色权限校验」拆开、分层执行——前者必须在业务 handler 里做,后者才能交给中间件统一拦。漏掉任何一层,水平越权就藏不住。


















