最佳实践与陷阱
本章汇总全手册的陷阱与最佳实践,并给出模型选型速查和上线前检查清单。各条目标注了详细讲解所在章节。
陷阱汇总
模型与匹配
| 🚨 陷阱 | 后果 | 正解 | 详见 |
|---|---|---|---|
忽略 Enforce 的 error |
配置错误被当成"无权限" | error 回 500 + 告警,!ok 才是 403 |
00 / 07 |
Enforce 参数顺序传错 |
不报错,永远 false | 参数顺序 = request_definition 顺序 | 01 |
| deny 策略配 allow-override effect | deny 完全不生效 | deny 必须配 !some(where (p.eft == deny)) |
02 |
== 匹配通配符策略 |
/api/* 永远匹配不上 |
用 keyMatch 系列函数 | 06 |
/foo 不匹配 /foo/* |
列表页 403 | 授两条或用正则 ^/foo(/.*)?$ |
06 |
| regexMatch 不加锚点 | DELETE 命中 ELET,方法越权 |
"^(GET|POST)$" |
06 |
| obj 带 query string | keyMatch 失败 | 传 c.Request.URL.Path |
06 / 07 |
domain 模型漏 r.dom == p.dom |
跨租户越权 | matcher 双条件 + 负向测试 | 03 |
| ABAC 字段小写 | 取不到值 | 结构体字段必须可导出 | 02 |
| CSV 含逗号字段不加引号 | 策略被拆错列 | "(GET)|(POST)" 加双引号 |
00 |
RBAC
| 🚨 陷阱 | 后果 | 正解 | 详见 |
|---|---|---|---|
| 用直接版 API 查继承权限 | 权限显示不全 | 用 GetImplicit* 系列 |
03 |
| 用户名与角色名撞名 | 意外继承 | user: / role: 前缀隔离 |
03 |
| 删角色只删 g 行 | 幽灵权限残留 | 用 DeleteRole / DeleteUser |
03 |
| 环形继承 | 行为依赖层级上限 | 写入前做环检测 | 03 |
存储与同步
| 🚨 陷阱 | 后果 | 正解 | 详见 |
|---|---|---|---|
| CSV 文件用于生产 | 无 AutoSave,重启丢数据 | gorm-adapter | 05 |
大策略集调 SavePolicy |
删全表重写,慢且有空窗 | 依赖 AutoSave 增量 | 04 |
过滤加载后 SavePolicy |
部分覆盖全部,策略丢失 | 过滤模式只用增量 API | 05 |
| 绕过 API 直改数据库 | 内存与库不一致 | 只通过 Casbin API 改策略 | 05 |
| 多实例无 Watcher | 改权限"不生效" | redis-watcher + 兜底重载 | 08 |
| 依赖 pub/sub 不丢消息 | 断连期间策略不同步 | 定时 LoadPolicy 兜底 | 08 |
| 模型字段变更不迁移数据 | v 列整体错位 | 改模型 = 写数据迁移 | 05 |
并发与性能
| 🚨 陷阱 | 后果 | 正解 | 详见 |
|---|---|---|---|
| Web 服务用裸 Enforcer | data race | SyncedEnforcer |
01 |
| 每请求 NewEnforcer | 每次全量加载策略 | 进程级单例 | 07 |
| 自定义函数里做 IO | 每条策略一次 IO | 纯内存计算,数据前置传入 | 06 |
| 逐条导入大批策略 | 通知风暴 + 重载风暴 | AddPolicies 批量 |
08 |
| eval 策略泛滥 | 每次判定重编译表达式 | 控制数量,管住写入口 | 06 |
模型选型速查
只有几个用户,权限直挂 ────────────────→ ACL
标准后台(用户-角色-权限)───────────────→ RBAC
SaaS 多租户 ───────────────────────────→ RBAC with domains
保护 REST API ─────────────────────────→ RBAC + keyMatch2 + regexMatch
"只能操作自己的资源" ────────────────────→ ABAC(或 keyGet)
白名单 + 个别封禁 ──────────────────────→ allow-and-deny + eft 字段
规则要运营可配、带条件 ──────────────────→ eval() 动态规则(管住入口)💡 最佳实践清单
设计
- 模型进代码库,策略进数据库:model.conf 用
go:embed编译进二进制、随版本评审;策略用 gorm-adapter 存库 - ID 加命名空间前缀:
user:1001、role:editor、tenant:42,杜绝撞名 - Casbin 只存权限关系:角色显示名、描述等业务字段放业务表,用 key 关联
- 两层防线:中间件管 API 面(RBAC),业务层管对象面(ABAC)
- 超管写在 matcher 里(
|| r.sub == "root"),不散落在业务 if 里
编码
- Web 服务一律
SyncedEnforcer单例 - Enforce 的两个返回值都处理:err → 500 + 告警;!ok → 403
- 改模型必写测试:核心用例(正向 + 越权负向)做成表驱动单测;改 matcher 先在 Casbin Editor 验证
- 用
EnforceEx做审计与排障:记录命中的策略行 - 批量操作用批量 API:
AddPolicies/BatchEnforce
运维
- 多实例 = adapter + Watcher + 定时兜底重载,三件缺一不可
- 策略变更要有审计日志:谁、何时、改了哪条
- 权限管理接口本身要授权:只有超管角色可调
- 上线前做负向测试:普通用户访问管理接口、A 租户访问 B 租户资源,必须 403
表驱动测试模板
func TestAuthz(t *testing.T) {
e := newTestEnforcer(t) // 加载真实 model + 测试策略
tests := []struct {
name string
sub, obj, act string
want bool
}{
{"admin 可删用户", "role:admin", "/api/users/1", "DELETE", true},
{"editor 可发文章", "user:bob", "/api/articles", "POST", true},
{"viewer 不可发文章", "user:carol", "/api/articles", "POST", false}, // 负向
{"跨租户拒绝", "user:alice", "/api/tenant2/data", "GET", false}, // 负向
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := e.Enforce(tt.sub, tt.obj, tt.act)
if err != nil {
t.Fatalf("enforce error: %v", err)
}
if got != tt.want {
t.Errorf("Enforce(%s, %s, %s) = %v, want %v",
tt.sub, tt.obj, tt.act, got, tt.want)
}
})
}
}生产检查清单
上线前逐项确认:
- 使用
github.com/casbin/casbin/v3(不是 v1/v2 旧包) - Enforcer 为
SyncedEnforcer(或 SyncedCached),进程级单例 - 策略存储为数据库 adapter,AutoSave 生效(写一条策略后查库确认)
- 多实例部署已配置 Watcher,并验证:实例 A 改策略,实例 B 秒级生效
- 有兜底重载(
StartAutoLoadPolicy或重连补偿) -
Enforce的 error 走 500 + 告警,不与 403 混淆 - 公开路由(登录/健康检查)不经过授权中间件
- 核心权限用例有表驱动单测,含越权负向用例
- domain 模型通过了跨租户负向测试
- 权限管理接口自身有授权 + 审计日志
- 策略列宽足够(长 URL/ID 不被 100 字符截断)
- 大批量策略导入用批量 API,导入后各实例已同步
参考资源
- 官方文档:https://casbin.apache.org/docs/overview(curl 获取 GitHub 资源,见仓库网络规则)
- 在线编辑器:https://casbin.org/editor/
- 核心仓库:
github.com/casbin/casbin(已捐入 Apache,镜像apache/casbin) - 适配器列表:https://casbin.org/docs/adapters
- Watcher 列表:https://casbin.org/docs/watchers
- 内置模型示例:仓库
examples/目录(各种 model.conf + policy.csv 成对出现,是最好的学习材料)