Skip to content
Go
适配器与持久化

适配器与持久化

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 加宽。