适配器与持久化
Adapter(适配器)负责策略的加载与保存,是 Casbin 与存储层之间的桥。本章讲清 Adapter 的接口分层、生产首选的 gorm-adapter、大策略集必备的过滤加载,以及如何自定义 adapter。
Adapter 生态一览
| Adapter | 存储 | 包 |
|---|---|---|
| File Adapter(内置) | CSV 文件 | casbin/v3/persist/file-adapter |
| String Adapter | 内存字符串 | 社区 qiangmzsx/string-adapter |
| Gorm Adapter | MySQL/PostgreSQL/SQLite/SQL Server | casbin/gorm-adapter/v3 |
| Xorm / SQLX / Ent Adapter | 关系库 | casbin/xorm-adapter 等 |
| MongoDB Adapter | MongoDB | casbin/mongodb-adapter |
| Redis Adapter | Redis | casbin/redis-adapter |
| Etcd / Consul Adapter | KV 存储 | 社区 |
💡 最佳实践:无特殊理由直接选 gorm-adapter——官方维护、支持全部扩展接口(增量/批量/过滤/更新/事务)、策略与业务数据同库便于备份与事务。
🔬 深入原理:Adapter 接口分层
Adapter 是一组渐进式接口,实现越多,Enforcer 能力越强:
// 基础接口:所有 adapter 必须实现
type Adapter interface {
LoadPolicy(model model.Model) error // 全量加载
SavePolicy(model model.Model) error // 全量保存(删全部+写全部)
AddPolicy(sec, ptype string, rule []string) error // 增量加(AutoSave 用)
RemovePolicy(sec, ptype string, rule []string) error // 增量删
RemoveFilteredPolicy(sec, ptype string, fieldIndex int, fieldValues ...string) error
}
// 可选扩展接口
type BatchAdapter interface { // AddPolicies / RemovePolicies 批量增量
AddPolicies(sec, ptype string, rules [][]string) error
RemovePolicies(sec, ptype string, rules [][]string) error
}
type FilteredAdapter interface { // LoadFilteredPolicy 部分加载
LoadFilteredPolicy(model model.Model, filter interface{}) error
IsFiltered() bool
}
type UpdatableAdapter interface { // UpdatePolicy 原子更新
UpdatePolicy(sec, ptype string, oldRule, newRule []string) error
}Enforcer 在调用时会做接口断言:adapter 没实现 BatchAdapter 却调 AddPolicies → 返回 “not implemented” 错误。这就是"同一个 API 换个 adapter 就报错"的原因。
| 能力 | file-adapter | gorm-adapter |
|---|---|---|
| LoadPolicy / SavePolicy | ✅ | ✅ |
| AutoSave 增量(Add/Remove) | ❌ | ✅ |
| 批量(AddPolicies) | ❌ | ✅ |
| 过滤加载(LoadFilteredPolicy) | ✅(FilteredAdapter 变体) | ✅ |
| UpdatePolicy | ❌ | ✅ |
| 事务 | ❌ | ✅ |
File Adapter(仅限开发/演示)
// 隐式:直接传文件路径
e, _ := casbin.NewEnforcer("model.conf", "policy.csv")
// 显式等价写法
import fileadapter "github.com/casbin/casbin/v3/persist/file-adapter"
a := fileadapter.NewAdapter("policy.csv")
e, _ := casbin.NewEnforcer("model.conf", a)🚨 陷阱:file adapter 不支持 AutoSave —— AddPolicy 只写内存,重启即丢,必须手动 e.SavePolicy() 落盘。且写文件无并发保护、无原子性,生产环境禁用。
Gorm Adapter(生产首选)
go get github.com/casbin/gorm-adapter/v3三种初始化方式
import (
"github.com/casbin/casbin/v3"
gormadapter "github.com/casbin/gorm-adapter/v3"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
// 方式一:DSN 不带库名 → 自动创建名为 casbin 的数据库
a, _ := gormadapter.NewAdapter("mysql", "user:pass@tcp(127.0.0.1:3306)/")
// 方式二:DSN 带库名 + 第三个参数 true → 使用已有库 abc,自动建表 casbin_rule
a, _ = gormadapter.NewAdapter("mysql", "user:pass@tcp(127.0.0.1:3306)/abc", true)
// 方式三(推荐):复用业务已有的 *gorm.DB 连接池
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
a, _ = gormadapter.NewAdapterByDB(db)
e, _ := casbin.NewSyncedEnforcer("model.conf", a)
e.LoadPolicy() // NewEnforcer 传入 adapter 时已自动加载,重复调用是幂等的💡 最佳实践:用 NewAdapterByDB 复用业务连接池——统一连接数管理、统一慢查询监控,还能与业务操作共享事务。
表结构
自动创建的 casbin_rule 表:
CREATE TABLE casbin_rule (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
ptype VARCHAR(100), -- "p" / "g" / "g2"...
v0 VARCHAR(100), -- p: sub g: user
v1 VARCHAR(100), -- p: obj g: role
v2 VARCHAR(100), -- p: act g: domain(如有)
v3 VARCHAR(100), v4 VARCHAR(100), v5 VARCHAR(100),
UNIQUE KEY unique_index (ptype, v0, v1, v2, v3, v4, v5)
);p, admin, /api/*, GET → ptype=p, v0=admin, v1=/api/*, v2=GET
g, alice, admin → ptype=g, v0=alice, v1=admin自定义表名 / 列宽
// 自定义表名(多个系统共库时隔离)
a, _ := gormadapter.NewAdapterByDBUseTableName(db, "myapp", "auth_rule")
// → 表名 myapp_auth_rule
// 自定义列宽(默认 100 不够放长 URL 时)
type CasbinRule struct {
ID uint `gorm:"primaryKey;autoIncrement"`
Ptype string `gorm:"size:512;uniqueIndex:unique_index"`
V0 string `gorm:"size:512;uniqueIndex:unique_index"`
V1 string `gorm:"size:512;uniqueIndex:unique_index"`
V2 string `gorm:"size:512;uniqueIndex:unique_index"`
V3 string `gorm:"size:512;uniqueIndex:unique_index"`
V4 string `gorm:"size:512;uniqueIndex:unique_index"`
V5 string `gorm:"size:512;uniqueIndex:unique_index"`
}
a, _ := gormadapter.NewAdapterByDBWithCustomTable(db, &CasbinRule{})🚨 陷阱:MySQL 的唯一索引长度上限(InnoDB 默认 3072 字节)——7 列都放 512 且 utf8mb4 时建索引会失败。加长个别列即可,不要全列拉满。
过滤加载:LoadFilteredPolicy
策略几十万条(典型:SaaS 每租户一套策略)时,全量加载慢且费内存。FilteredAdapter 允许只加载当前实例/租户相关的子集:
// gorm-adapter 的过滤器:按 ptype 和 v0~v5 过滤
e.LoadFilteredPolicy(gormadapter.Filter{
V1: []string{"tenant1"}, // 只加载 domain 为 tenant1 的 p/g 行
})
// 也可以按多字段
e.LoadFilteredPolicy(gormadapter.Filter{
Ptype: []string{"p"},
V0: []string{"admin", "editor"},
})
e.IsFiltered() // true:当前内存是部分策略🚨 陷阱:过滤加载后禁止调用 SavePolicy() —— 它会用内存中的"部分策略"覆盖存储里的"全部策略",等于删库。Casbin 会在 IsFiltered() 为 true 时拒绝 SavePolicy,但自定义流程仍需警惕。增量的 AddPolicy/RemovePolicy(AutoSave)不受影响,可正常使用。
💡 最佳实践:多租户 + 每租户独立 Enforcer 实例 + LoadFilteredPolicy 按租户加载,是大规模 SaaS 的标准做法;配合 LRU 缓存租户 Enforcer,冷租户自动逐出。
事务:策略与业务数据一起提交
场景:创建"项目"业务记录的同时给创建者授权,要求要么都成功要么都失败:
err := db.Transaction(func(tx *gorm.DB) error {
// 1. 业务写入
if err := tx.Create(&project).Error; err != nil {
return err
}
// 2. 用同一个 tx 建临时 adapter + enforcer 写策略
a, err := gormadapter.NewAdapterByDB(tx)
if err != nil {
return err
}
te, err := casbin.NewEnforcer(m, a) // m 为共享的 model
if err != nil {
return err
}
if _, err := te.AddPolicy(userID, "/projects/"+project.ID, ".*"); err != nil {
return err
}
return nil
})
// 提交成功后,让常驻 enforcer 重载(或通过 Watcher 通知,见 08 章)
if err == nil {
e.LoadPolicy()
}自定义 Adapter
实现基础 Adapter 接口即可接入任意存储。骨架:
import (
"github.com/casbin/casbin/v3/model"
"github.com/casbin/casbin/v3/persist"
)
type MyAdapter struct{ /* 存储客户端 */ }
func (a *MyAdapter) LoadPolicy(m model.Model) error {
// 从存储读出每行 → persist.LoadPolicyLine(line, m)
// line 形如 "p, alice, data1, read"
for _, line := range a.readAllLines() {
if err := persist.LoadPolicyLine(line, m); err != nil {
return err
}
}
return nil
}
func (a *MyAdapter) SavePolicy(m model.Model) error {
// 遍历 m["p"] 与 m["g"] 写回存储
return nil
}
// 不想支持增量时直接返回错误,Enforcer 会提示不支持 AutoSave
func (a *MyAdapter) AddPolicy(sec, ptype string, rule []string) error {
return errors.New("not implemented")
}
func (a *MyAdapter) RemovePolicy(sec, ptype string, rule []string) error {
return errors.New("not implemented")
}
func (a *MyAdapter) RemoveFilteredPolicy(sec, ptype string, fi int, fv ...string) error {
return errors.New("not implemented")
}💡 自定义 adapter 前先搜 casbin.org/docs/adapters——常见存储几乎都有现成实现。
常见陷阱
🚨 CSV 用于生产:无 AutoSave、无并发保护、容器重启丢数据。见到 NewEnforcer("model.conf", "policy.csv") 出现在生产代码就该报警。
🚨 多实例共用一个 DB 但各自内存:adapter 只解决持久化,不解决多实例内存同步。实例 A AddPolicy 后,实例 B 的内存策略是旧的,直到 LoadPolicy。多实例必须上 Watcher(见 08-分布式与高性能)。
🚨 过滤加载后 SavePolicy:部分覆盖全部,策略批量丢失。过滤模式下只用增量 API。
🚨 绕过 Casbin 直改 casbin_rule 表:DBA 手动 UPDATE 后内存不知情,判定仍按旧策略。任何直改数据库的操作后必须触发全实例 LoadPolicy。原则:策略只通过 Casbin API 修改。
🚨 v0~v5 与模型字段的对应关系靠位置:模型加字段(如插入 priority 到第一位)后,表里旧数据的 v0 含义全变。模型字段变更 = 数据迁移,要写迁移脚本重排 v 列。
🚨 默认列宽 100 截断长路径:URL 或资源 ID 超过 100 字符会被截断或写入失败,用 NewAdapterByDBWithCustomTable 加宽。