Casbin 学习教程(Go 版)
本文档面向正在学习 Golang 的开发者,从零开始系统讲解 Casbin 权限控制框架。
读完并完成文末练习后,你应该能够:理解 Casbin 的核心模型、编写 Model/Policy 文件、在 Go 项目中集成 Casbin,并实现 RBAC、ABAC 等常见权限方案。
目录
- [Casbin 是什么](#Casbin 是什么)
- 核心概念速览
- [PERM 元模型](#PERM 元模型)
- [Model 文件详解](#Model 文件详解)
- [Policy 策略文件详解](#Policy 策略文件详解)
- [Go 快速上手](#Go 快速上手)
- [RBAC 角色权限](#RBAC 角色权限)
- [RBAC with Domains(多租户)](#RBAC with Domains(多租户))
- [ABAC 属性权限](#ABAC 属性权限)
- [RESTful 路径匹配](#RESTful 路径匹配)
- [Adapter 持久化](#Adapter 持久化)
- 生产环境真实做法(重要)
- [Enforcer API 常用方法](#Enforcer API 常用方法)
- [在 Web 项目中集成](#在 Web 项目中集成)
- 最佳实践与常见坑
- 动手练习
- 参考资源
1. Casbin 是什么
Casbin 是一个开源的、支持多种访问控制模型的**授权(Authorization)**库。
需要区分两个概念:
| 概念 | 英文 | 职责 | 常见方案 |
|---|---|---|---|
| 认证 | Authentication | 确认「你是谁」 | JWT、Session、OAuth |
| 授权 | Authorization | 确认「你能做什么」 | Casbin、OPA |
Casbin 不负责登录,只负责在已知用户身份后,判断该用户是否允许执行某个操作。
为什么用 Casbin?
- 模型与策略分离:权限规则写在配置文件里,改规则不必改业务代码
- 表达力强:支持 ACL、RBAC、ABAC、RESTful 等
- 跨语言:Go、Java、Python、Node.js 等均有官方 SDK
- 可持久化:策略可存文件、MySQL、Redis 等
2. 核心概念速览
Casbin 的工作可以概括为四件事:
请求(Request) + 策略(Policy) → 匹配器(Matcher) → 效果(Effect) → 允许/拒绝
四个核心组件:
| 组件 | 说明 | 存放位置 |
|---|---|---|
| Model | 定义权限模型结构(请求长什么样、怎么匹配) | model.conf |
| Policy | 具体的权限规则数据 | policy.csv 或数据库 |
| Enforcer | 执行引擎,加载 Model + Policy 并做判断 | Go 代码中创建 |
| Adapter | 策略持久化适配器(文件、DB 等) | 可选 |
一次鉴权的完整流程
#mermaid-svg-O5X6lmriNG2SlJBu{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-O5X6lmriNG2SlJBu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-O5X6lmriNG2SlJBu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-O5X6lmriNG2SlJBu .error-icon{fill:#552222;}#mermaid-svg-O5X6lmriNG2SlJBu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-O5X6lmriNG2SlJBu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-O5X6lmriNG2SlJBu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-O5X6lmriNG2SlJBu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-O5X6lmriNG2SlJBu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-O5X6lmriNG2SlJBu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-O5X6lmriNG2SlJBu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-O5X6lmriNG2SlJBu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-O5X6lmriNG2SlJBu .marker.cross{stroke:#333333;}#mermaid-svg-O5X6lmriNG2SlJBu svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-O5X6lmriNG2SlJBu p{margin:0;}#mermaid-svg-O5X6lmriNG2SlJBu .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-O5X6lmriNG2SlJBu .cluster-label text{fill:#333;}#mermaid-svg-O5X6lmriNG2SlJBu .cluster-label span{color:#333;}#mermaid-svg-O5X6lmriNG2SlJBu .cluster-label span p{background-color:transparent;}#mermaid-svg-O5X6lmriNG2SlJBu .label text,#mermaid-svg-O5X6lmriNG2SlJBu span{fill:#333;color:#333;}#mermaid-svg-O5X6lmriNG2SlJBu .node rect,#mermaid-svg-O5X6lmriNG2SlJBu .node circle,#mermaid-svg-O5X6lmriNG2SlJBu .node ellipse,#mermaid-svg-O5X6lmriNG2SlJBu .node polygon,#mermaid-svg-O5X6lmriNG2SlJBu .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-O5X6lmriNG2SlJBu .rough-node .label text,#mermaid-svg-O5X6lmriNG2SlJBu .node .label text,#mermaid-svg-O5X6lmriNG2SlJBu .image-shape .label,#mermaid-svg-O5X6lmriNG2SlJBu .icon-shape .label{text-anchor:middle;}#mermaid-svg-O5X6lmriNG2SlJBu .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-O5X6lmriNG2SlJBu .rough-node .label,#mermaid-svg-O5X6lmriNG2SlJBu .node .label,#mermaid-svg-O5X6lmriNG2SlJBu .image-shape .label,#mermaid-svg-O5X6lmriNG2SlJBu .icon-shape .label{text-align:center;}#mermaid-svg-O5X6lmriNG2SlJBu .node.clickable{cursor:pointer;}#mermaid-svg-O5X6lmriNG2SlJBu .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-O5X6lmriNG2SlJBu .arrowheadPath{fill:#333333;}#mermaid-svg-O5X6lmriNG2SlJBu .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-O5X6lmriNG2SlJBu .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-O5X6lmriNG2SlJBu .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-O5X6lmriNG2SlJBu .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-O5X6lmriNG2SlJBu .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-O5X6lmriNG2SlJBu .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-O5X6lmriNG2SlJBu .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-O5X6lmriNG2SlJBu .cluster text{fill:#333;}#mermaid-svg-O5X6lmriNG2SlJBu .cluster span{color:#333;}#mermaid-svg-O5X6lmriNG2SlJBu div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-O5X6lmriNG2SlJBu .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-O5X6lmriNG2SlJBu rect.text{fill:none;stroke-width:0;}#mermaid-svg-O5X6lmriNG2SlJBu .icon-shape,#mermaid-svg-O5X6lmriNG2SlJBu .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-O5X6lmriNG2SlJBu .icon-shape p,#mermaid-svg-O5X6lmriNG2SlJBu .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-O5X6lmriNG2SlJBu .icon-shape .label rect,#mermaid-svg-O5X6lmriNG2SlJBu .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-O5X6lmriNG2SlJBu .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-O5X6lmriNG2SlJBu .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-O5X6lmriNG2SlJBu :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
应用收到请求
构造 Casbin 请求参数
Enforcer.Enforce
Matcher 匹配 Policy?
Effect: allow
Effect: deny
放行
403 Forbidden
3. PERM 元模型
Casbin 基于 PERM 元模型(Policy、Effect、Request、Matcher):
+--------+ +--------+ +----------+ +--------+
| Request| --> | Policy | --> | Matcher | --> | Effect |
+--------+ +--------+ +----------+ +--------+
谁要什么 规则库 匹配逻辑 最终决策
- Request(r) :一次鉴权请求的参数,如
(用户, 资源, 动作) - Policy(p):存储的权限规则
- Matcher(m):表达式,决定 Request 与 Policy 是否匹配
- Effect(e) :多条规则命中时的汇总策略(
some(where (p.eft == allow))表示有一条 allow 就通过)
4. Model 文件详解
Model 文件通常命名为 model.conf,由多个 [section] 组成。
4.1 最简单的 ACL 模型
ini
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
逐段解释
[request_definition] --- 定义请求元组
r = sub, obj, act
| 字段 | 含义 | 示例 |
|---|---|---|
sub |
Subject,主体(用户/角色) | alice |
obj |
Object,资源 | data1 |
act |
Action,动作 | read |
[policy_definition] --- 定义策略元组,结构与 request 对应
[policy_effect] --- 最终效果
ini
e = some(where (p.eft == allow))
含义:只要存在一条 allow 策略匹配,就允许访问。这是最常见的写法。
另一种写法(白名单 + 显式 deny):
ini
e = some(where (p.eft == allow)) && !some(where (p.eft == deny))
[matchers] --- 匹配表达式
ini
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
Casbin 内置了 keyMatch、keyMatch2、regexMatch 等函数,后面会用到。
4.2 带角色的 RBAC 模型
ini
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
[role_definition] 是 RBAC 的关键:
ini
g = _, _
表示 g 是一个二元关系:g, 用户, 角色,即「用户拥有某角色」。
g(r.sub, p.sub) 的含义:请求中的用户 r.sub 是否拥有策略中定义的角色 p.sub。
5. Policy 策略文件详解
重要说明 :
policy.csv主要用于学习、本地调试和单元测试 。实际项目中几乎不会让人手工改 CSV,而是通过数据库 + 管理后台/API 动态维护策略。详见 [第 12 章](#第 12 章)。
Policy 文件通常命名为 policy.csv,每行一条规则。
5.1 ACL 策略示例
对应上面的 ACL 模型:
csv
p, alice, data1, read
p, alice, data1, write
p, bob, data2, read
格式:p, 字段1, 字段2, 字段3(p 表示 policy 行)
5.2 RBAC 策略示例
csv
p, admin, data1, read
p, admin, data1, write
p, admin, data2, read
p, admin, data2, write
p, user, data1, read
g, alice, admin
g, bob, user
p行:定义角色能做什么g行:定义用户 拥有什么角色
因此 alice 通过 g 继承 admin 的全部权限。
5.3 带 deny 的策略
Casbin 默认每条 policy 的 effect 为 allow。也可以显式写 deny:
csv
p, alice, data1, read, allow
p, alice, data1, write, deny
需要在 policy_definition 中增加 eft 字段:
ini
p = sub, obj, act, eft
6. Go 快速上手
6.1 环境准备
bash
# 创建项目目录
mkdir casbin-demo && cd casbin-demo
go mod init casbin-demo
# 安装 Casbin
go get github.com/casbin/casbin/v2
6.2 项目结构
casbin-demo/
├── go.mod
├── main.go
├── model.conf
└── policy.csv
6.3 创建 model.conf
ini
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
6.4 创建 policy.csv
csv
p, alice, data1, read
p, bob, data2, write
6.5 编写 main.go
go
package main
import (
"fmt"
"log"
"github.com/casbin/casbin/v2"
)
func main() {
// 创建 Enforcer:传入 model 路径和 policy 路径
e, err := casbin.NewEnforcer("model.conf", "policy.csv")
if err != nil {
log.Fatalf("创建 Enforcer 失败: %v", err)
}
// Enforce(主体, 资源, 动作) → bool
ok, _ := e.Enforce("alice", "data1", "read")
fmt.Println("alice 读 data1:", ok) // true
ok, _ = e.Enforce("alice", "data1", "write")
fmt.Println("alice 写 data1:", ok) // false
ok, _ = e.Enforce("bob", "data2", "write")
fmt.Println("bob 写 data2:", ok) // true
}
6.6 运行
bash
go run main.go
预期输出:
alice 读 data1: true
alice 写 data1: false
bob 写 data2: true
6.7 代码讲解
-
NewEnforcer(modelPath, policyPath)加载模型和策略,返回执行引擎。也可以只传 model,后续用
LoadPolicy()加载。 -
Enforce(...)参数顺序必须与
[request_definition]中定义的字段一致。返回(bool, error)。 -
错误处理
生产环境务必检查
error;教程为简洁有时用_忽略。
7. RBAC 角色权限
RBAC(Role-Based Access Control)是 Casbin 最常用的模式。
7.1 模型文件 model_rbac.conf
ini
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
7.2 策略文件 policy_rbac.csv
csv
p, admin, /api/users, GET
p, admin, /api/users, POST
p, admin, /api/users, DELETE
p, editor, /api/articles, GET
p, editor, /api/articles, POST
p, viewer, /api/articles, GET
g, alice, admin
g, bob, editor
g, charlie, viewer
7.3 Go 示例
go
package main
import (
"fmt"
"log"
"github.com/casbin/casbin/v2"
)
func main() {
e, err := casbin.NewEnforcer("model_rbac.conf", "policy_rbac.csv")
if err != nil {
log.Fatal(err)
}
tests := []struct {
user, obj, act string
want bool
}{
{"alice", "/api/users", "DELETE", true},
{"bob", "/api/articles", "POST", true},
{"bob", "/api/users", "GET", false},
{"charlie", "/api/articles", "GET", true},
{"charlie", "/api/articles", "POST", false},
}
for _, t := range tests {
ok, _ := e.Enforce(t.user, t.obj, t.act)
status := "✓"
if ok != t.want {
status = "✗"
}
fmt.Printf("%s %s %s %s → %v (期望 %v)\n", status, t.user, t.act, t.obj, ok, t.want)
}
}
7.4 角色继承
Casbin 支持角色的层级继承:
csv
g, admin, superadmin
g, alice, admin
g 可以嵌套:alice → admin → superadmin。
在 matcher 中使用 g 的变体实现传递闭包:
ini
[role_definition]
g = _, _
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
Casbin 的 g 函数默认支持多级继承(递归查找)。
7.5 动态管理角色
go
// 给用户添加角色
_, err := e.AddRoleForUser("david", "editor")
// 删除用户的角色
_, err = e.DeleteRoleForUser("david", "editor")
// 获取用户的所有角色
roles, err := e.GetRolesForUser("alice")
// 获取拥有某角色的所有用户
users, err := e.GetUsersForRole("admin")
// 保存到 policy 文件(若使用文件 Adapter)
err = e.SavePolicy()
8. RBAC with Domains(多租户)
多租户 SaaS 场景中,同一用户在不同租户(domain)可能有不同角色。
8.1 模型
ini
[request_definition]
r = sub, dom, obj, act
[policy_definition]
p = sub, dom, obj, act
[role_definition]
g = _, _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act
注意 g = _, _, _ 是三元组:用户, 角色, 域。
8.2 策略
csv
p, admin, tenant1, data1, read
p, admin, tenant1, data1, write
p, user, tenant1, data1, read
p, admin, tenant2, data2, read
g, alice, admin, tenant1
g, bob, user, tenant1
g, alice, user, tenant2
8.3 Go 调用
go
// alice 在 tenant1 是 admin,可以写
ok, _ := e.Enforce("alice", "tenant1", "data1", "write") // true
// alice 在 tenant2 只是 user,tenant2 的 data2 没有 user 读权限
ok, _ = e.Enforce("alice", "tenant2", "data2", "read") // false
9. ABAC 属性权限
ABAC(Attribute-Based Access Control)根据属性做决策,比 RBAC 更灵活。
9.1 场景
规则:「年龄小于 18 的用户不能购买 alcohol 类商品」。
9.2 模型
ini
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub.Age >= 18 && r.obj.Category != "alcohol" || r.obj.Category == "alcohol" && r.sub.Age >= 18
更清晰的写法:
ini
[matchers]
m = r.sub.Age >= 18 || r.obj.Category != "alcohol"
9.3 Go 中使用结构体
go
package main
import (
"fmt"
"log"
"github.com/casbin/casbin/v2"
)
type User struct {
Name string
Age int
}
type Resource struct {
Name string
Category string
}
func main() {
e, err := casbin.NewEnforcer("model_abac.conf", "policy_abac.csv")
if err != nil {
log.Fatal(err)
}
alice := User{Name: "alice", Age: 25}
bob := User{Name: "bob", Age: 16}
book := Resource{Name: "Go语言编程", Category: "book"}
wine := Resource{Name: "红酒", Category: "alcohol"}
fmt.Println(e.Enforce(alice, book, "buy")) // true
fmt.Println(e.Enforce(bob, book, "buy")) // true
fmt.Println(e.Enforce(alice, wine, "buy")) // true
fmt.Println(e.Enforce(bob, wine, "buy")) // false
}
ABAC 的 policy.csv 可以为空,规则全在 matcher 中;也可以结合 policy 做混合模型。
9.4 内置匹配函数
Casbin 在 matcher 中提供了丰富的函数:
| 函数 | 说明 | 示例 |
|---|---|---|
keyMatch(key1, key2) |
路径前缀匹配 | keyMatch("/foo/bar", "/foo/*") → true |
keyMatch2(key1, key2) |
带 :id 的 RESTful 匹配 |
keyMatch2("/foo/1", "/foo/:id") → true |
keyMatch3(key1, key2) |
忽略 {} 内内容 |
|
regexMatch(key1, pattern) |
正则匹配 | |
ipMatch(ip1, ip2) |
IP 段匹配 | |
globMatch(str, pattern) |
Glob 模式匹配 |
10. RESTful 路径匹配
Web API 权限常用路径 + HTTP Method 作为 (obj, act)。
10.1 模型
ini
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && keyMatch2(r.obj, p.obj) && regexMatch(r.act, p.act)
10.2 策略
csv
p, admin, /api/users, (GET|POST|PUT|DELETE)
p, user, /api/users/:id, GET
p, user, /api/profile, GET
g, alice, admin
g, bob, user
10.3 测试
go
e.Enforce("bob", "/api/users/123", "GET") // true(匹配 /api/users/:id)
e.Enforce("bob", "/api/users", "POST") // false
e.Enforce("alice", "/api/users", "DELETE") // true
11. Adapter 持久化
默认 NewEnforcer 使用文件 Adapter,策略存在 CSV 中。生产环境通常用数据库。
11.1 文件 Adapter(默认)
go
e, _ := casbin.NewEnforcer("model.conf", "policy.csv")
11.2 MySQL Adapter
bash
go get github.com/casbin/gorm-adapter/v3
go
import (
"github.com/casbin/casbin/v2"
gormadapter "github.com/casbin/gorm-adapter/v3"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
dsn := "user:password@tcp(127.0.0.1:3306)/casbin?charset=utf8mb4&parseTime=True"
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
adapter, _ := gormadapter.NewAdapterByDB(db)
e, _ := casbin.NewEnforcer("model.conf", adapter)
// 从数据库加载策略
_ = e.LoadPolicy()
// 动态添加规则后保存
_, _ = e.AddPolicy("alice", "data3", "read")
_ = e.SavePolicy()
}
11.3 Redis Watcher(多实例同步)
多个服务实例共享策略时,一个实例修改策略后需通知其他实例 reload:
bash
go get github.com/casbin/redis-watcher/v2
go
w, _ := rediswatcher.NewWatcher("127.0.0.1:6379", rediswatcher.WatcherOptions{})
_ = e.SetWatcher(w)
_ = w.SetUpdateCallback(func(msg string) {
_ = e.LoadPolicy()
})
12. 生产环境真实做法(重要)
你的直觉是对的:线上项目基本不会通过 CSV 文件维护权限。CSV 在前面的章节出现,是因为它能帮你快速理解 Casbin 的策略格式;真正上线时,做法完全不同。
12.1 开发 vs 生产:对比一览
| 维度 | 学习 / 本地开发 | 生产环境 |
|---|---|---|
| Policy 存放 | policy.csv 文件 |
MySQL / PostgreSQL 等数据库 |
| Policy 修改方式 | 手工编辑文件 | 管理后台 或 REST API 动态增删 |
| Model 存放 | model.conf 文件 |
仍是文件居多(改动少),或嵌入代码 |
| 多实例部署 | 单机,无感 | 需要 Watcher 通知其他节点 reload |
| 谁维护权限 | 开发者 | 运营 / 管理员通过后台操作 |
12.2 生产环境的典型架构
#mermaid-svg-gKO8DfDKPOYq45kD{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gKO8DfDKPOYq45kD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gKO8DfDKPOYq45kD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gKO8DfDKPOYq45kD .error-icon{fill:#552222;}#mermaid-svg-gKO8DfDKPOYq45kD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gKO8DfDKPOYq45kD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gKO8DfDKPOYq45kD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gKO8DfDKPOYq45kD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gKO8DfDKPOYq45kD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gKO8DfDKPOYq45kD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gKO8DfDKPOYq45kD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gKO8DfDKPOYq45kD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gKO8DfDKPOYq45kD .marker.cross{stroke:#333333;}#mermaid-svg-gKO8DfDKPOYq45kD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gKO8DfDKPOYq45kD p{margin:0;}#mermaid-svg-gKO8DfDKPOYq45kD .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-gKO8DfDKPOYq45kD .cluster-label text{fill:#333;}#mermaid-svg-gKO8DfDKPOYq45kD .cluster-label span{color:#333;}#mermaid-svg-gKO8DfDKPOYq45kD .cluster-label span p{background-color:transparent;}#mermaid-svg-gKO8DfDKPOYq45kD .label text,#mermaid-svg-gKO8DfDKPOYq45kD span{fill:#333;color:#333;}#mermaid-svg-gKO8DfDKPOYq45kD .node rect,#mermaid-svg-gKO8DfDKPOYq45kD .node circle,#mermaid-svg-gKO8DfDKPOYq45kD .node ellipse,#mermaid-svg-gKO8DfDKPOYq45kD .node polygon,#mermaid-svg-gKO8DfDKPOYq45kD .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gKO8DfDKPOYq45kD .rough-node .label text,#mermaid-svg-gKO8DfDKPOYq45kD .node .label text,#mermaid-svg-gKO8DfDKPOYq45kD .image-shape .label,#mermaid-svg-gKO8DfDKPOYq45kD .icon-shape .label{text-anchor:middle;}#mermaid-svg-gKO8DfDKPOYq45kD .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gKO8DfDKPOYq45kD .rough-node .label,#mermaid-svg-gKO8DfDKPOYq45kD .node .label,#mermaid-svg-gKO8DfDKPOYq45kD .image-shape .label,#mermaid-svg-gKO8DfDKPOYq45kD .icon-shape .label{text-align:center;}#mermaid-svg-gKO8DfDKPOYq45kD .node.clickable{cursor:pointer;}#mermaid-svg-gKO8DfDKPOYq45kD .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gKO8DfDKPOYq45kD .arrowheadPath{fill:#333333;}#mermaid-svg-gKO8DfDKPOYq45kD .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gKO8DfDKPOYq45kD .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gKO8DfDKPOYq45kD .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gKO8DfDKPOYq45kD .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gKO8DfDKPOYq45kD .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gKO8DfDKPOYq45kD .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gKO8DfDKPOYq45kD .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gKO8DfDKPOYq45kD .cluster text{fill:#333;}#mermaid-svg-gKO8DfDKPOYq45kD .cluster span{color:#333;}#mermaid-svg-gKO8DfDKPOYq45kD div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-gKO8DfDKPOYq45kD .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gKO8DfDKPOYq45kD rect.text{fill:none;stroke-width:0;}#mermaid-svg-gKO8DfDKPOYq45kD .icon-shape,#mermaid-svg-gKO8DfDKPOYq45kD .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gKO8DfDKPOYq45kD .icon-shape p,#mermaid-svg-gKO8DfDKPOYq45kD .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gKO8DfDKPOYq45kD .icon-shape .label rect,#mermaid-svg-gKO8DfDKPOYq45kD .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gKO8DfDKPOYq45kD .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gKO8DfDKPOYq45kD .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gKO8DfDKPOYq45kD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 业务请求
后端服务
管理端
允许
拒绝
AddRoleForUser / AddPolicy
SavePolicy
Watcher 通知
权限管理后台
角色/菜单/接口配置
权限 API
Casbin Enforcer
casbin_rule 表
Enforce 鉴权
用户请求 API
JWT 认证
业务 Handler
403
其他服务实例 Reload
核心思路:
- Model(model.conf):定义「权限怎么判断」,上线后很少改,继续用配置文件或内嵌字符串即可。
- Policy(策略数据) :存在数据库的
casbin_rule表里,通过代码 API 读写,不手工改文件。 - 管理后台:管理员勾选「角色能访问哪些菜单/接口」,后端调用 Casbin API 写入数据库。
12.3 数据库表长什么样
使用 gorm-adapter 后,Casbin 会自动建一张表(默认名 casbin_rule):
| id | ptype | v0 | v1 | v2 | v3 | v4 | v5 |
|---|---|---|---|---|---|---|---|
| 1 | p | admin | /api/users | GET | |||
| 2 | p | admin | /api/users | POST | |||
| 3 | g | alice | admin |
ptype = p:权限规则(对应 policy.csv 的p, ...)ptype = g:角色绑定(对应 policy.csv 的g, alice, admin)v0, v1, v2...:就是 CSV 里逗号后面的各字段
你在后台做的每一次「给用户分配角色」,底层就是往这张表插一条 g 记录。
12.4 生产级初始化代码
go
package casbinx
import (
"sync"
"github.com/casbin/casbin/v2"
gormadapter "github.com/casbin/gorm-adapter/v3"
"gorm.io/gorm"
)
var (
enforcer *casbin.Enforcer
once sync.Once
)
// Init 应用启动时调用一次,全局单例
func Init(db *gorm.DB, modelPath string) error {
var err error
once.Do(func() {
adapter, e := gormadapter.NewAdapterByDB(db)
if e != nil {
err = e
return
}
enforcer, e = casbin.NewEnforcer(modelPath, adapter)
if e != nil {
err = e
return
}
_ = enforcer.LoadPolicy()
})
return err
}
func Enforcer() *casbin.Enforcer {
return enforcer
}
go
// main.go
func main() {
db := initDB()
if err := casbinx.Init(db, "configs/rbac_model.conf"); err != nil {
log.Fatal(err)
}
// 启动 HTTP 服务...
}
12.5 权限管理 API(这才是日常开发写的)
管理员在后台操作,后端提供类似这样的接口:
go
// POST /admin/roles/:roleId/permissions 给角色添加接口权限
func AddRolePermission(c *gin.Context) {
role := c.Param("roleId")
path := c.PostForm("path") // /api/users
method := c.PostForm("method") // GET
e := casbinx.Enforcer()
added, err := e.AddPolicy(role, path, method)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
if added {
_ = e.SavePolicy() // 持久化到数据库
}
c.JSON(200, gin.H{"ok": true})
}
// POST /admin/users/:userId/roles 给用户分配角色
func AssignUserRole(c *gin.Context) {
userID := c.Param("userId")
role := c.PostForm("role")
e := casbinx.Enforcer()
_, err := e.AddRoleForUser(userID, role)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
_ = e.SavePolicy()
c.JSON(200, gin.H{"ok": true})
}
// DELETE /admin/users/:userId/roles/:role 移除用户角色
func RemoveUserRole(c *gin.Context) {
userID := c.Param("userId")
role := c.Param("role")
e := casbinx.Enforcer()
_, _ = e.DeleteRoleForUser(userID, role)
_ = e.SavePolicy()
c.JSON(200, gin.H{"ok": true})
}
// GET /admin/users/:userId/permissions 查询用户拥有的所有权限
func GetUserPermissions(c *gin.Context) {
userID := c.Param("userId")
e := casbinx.Enforcer()
perms, _ := e.GetImplicitPermissionsForUser(userID)
c.JSON(200, gin.H{"permissions": perms})
}
日常开发流程:写这些管理 API + 鉴权中间件,而不是编辑 CSV。
12.6 首次部署:种子数据(Seed)
虽然不用 CSV,但项目第一次部署时,通常需要初始化默认角色和权限(如 superadmin)。做法是用代码写入,而不是维护一个 policy.csv:
go
// 应用启动时执行一次,或通过 migration 脚本执行
func SeedDefaultPolicies(e *casbin.Enforcer) error {
defaults := [][]string{
{"p", "superadmin", "/api/*", "(GET|POST|PUT|DELETE)"},
{"p", "admin", "/api/users", "(GET|POST)"},
{"g", "admin", "superadmin"}, // 可选:角色继承
}
for _, rule := range defaults {
ptype := rule[0]
params := rule[1:]
if ptype == "p" {
exists, _ := e.HasPolicy(params)
if !exists {
_, _ = e.AddPolicy(params)
}
}
if ptype == "g" {
_, _ = e.AddGroupingPolicy(params)
}
}
return e.SavePolicy()
}
12.7 和自建 RBAC 表的关系
很多项目本身已有 sys_user、sys_role、sys_menu 等表。常见两种集成方式:
方式 A:Casbin 作为唯一策略源(推荐,简单)
用户表 sys_user ──→ user_id
角色表 sys_role ──→ role_code ──→ 同步到 casbin_rule (g 规则)
菜单/接口 ──→ path+method ──→ 同步到 casbin_rule (p 规则)
后台改角色权限时,同时更新 Casbin(AddPolicy + SavePolicy)。
方式 B:自建表为主,Casbin 只做鉴权引擎
业务表是主数据,每次变更后调用 LoadPolicy() 或自定义 Adapter 从业务表读取。适合已有成熟 RBAC 系统的迁移项目。
方式 C:不用 Casbin 存 Policy,只复用 Model + Matcher
少数团队把权限存在 Redis/业务表里,鉴权时手动构造参数调用 Enforce。这种用法较少,一般直接用 Adapter 更省心。
12.8 Model 文件在生产中放哪
| 做法 | 说明 |
|---|---|
配置文件 configs/rbac_model.conf |
最常见,随代码仓库管理,变更走 Code Review |
嵌入代码 //go:embed model.conf |
容器化部署时少一个外部文件依赖 |
| 配置中心 | 超大团队、需热更新 Model 时(极少见) |
Model 定义的是「匹配逻辑」,上线后通常几个月才改一次;真正频繁变动的是 Policy(谁有什么权限),所以 Policy 放数据库,Model 放文件是业界主流。
12.9 一次完整的权限变更链路
以「给运营小王开通 editor 角色」为例:
1. 管理员在后台点击「分配角色」
2. 前端 POST /admin/users/1001/roles { role: "editor" }
3. 后端 e.AddRoleForUser("1001", "editor")
4. 后端 e.SavePolicy() → 写入 casbin_rule 表
5. (多实例)Watcher 广播 → 其他 Pod 执行 LoadPolicy()
6. 小王下次请求 → 中间件 e.Enforce("1001", "/api/articles", "POST") → true
12.10 小结:你需要记住的
学习阶段:model.conf + policy.csv ← 理解格式用
生产阶段:model.conf + 数据库 + 管理API ← 实际干活用
- CSV 不是生产方案,只是 Casbin 策略的「文本表示形式」
- 数据库里的
casbin_rule表,本质上就是 CSV 的内容结构化存储 - 你的业务代码重点是:Enforcer 单例 + 鉴权中间件 + 权限管理 CRUD API
13. Enforcer API 常用方法
13.1 鉴权
go
ok, err := e.Enforce(sub, obj, act)
ok, err := e.Enforce(sub, dom, obj, act) // 多租户
13.2 策略 CRUD
go
// 添加
e.AddPolicy("alice", "data1", "read")
e.AddPolicies([][]string{{"alice", "data2", "read"}, {"bob", "data2", "write"}})
// 删除
e.RemovePolicy("alice", "data1", "read")
e.RemoveFilteredPolicy(0, "alice") // 删除 alice 的所有策略
// 查询
e.GetPolicy() // 所有 p 规则
e.GetFilteredPolicy(0, "alice") // 第一字段为 alice 的规则
e.HasPolicy("alice", "data1", "read") // 是否存在
13.3 角色管理
go
e.AddRoleForUser("alice", "admin")
e.DeleteRoleForUser("alice", "admin")
e.DeleteRolesForUser("alice")
e.GetRolesForUser("alice")
e.GetUsersForRole("admin")
e.HasRoleForUser("alice", "admin")
13.4 批量鉴权
go
// 获取用户所有允许的权限(需配合 GetFilteredPolicy 或自定义)
roles, _ := e.GetImplicitRolesForUser("alice")
permissions, _ := e.GetPermissionsForUser("alice")
13.5 其他实用方法
go
e.LoadPolicy() // 重新加载策略
e.SavePolicy() // 保存策略
e.ClearPolicy() // 清空内存中的策略
e.EnableEnforce(true) // 启用/禁用鉴权(禁用后 Enforce 全返回 true)
e.EnableLog(true) // 开启调试日志
14. 在 Web 项目中集成
以下以 Gin 为例(Casbin 也提供官方 middleware)。
14.1 安装
bash
go get github.com/gin-gonic/gin
go get github.com/casbin/casbin/v2
14.2 中间件示例
go
package middleware
import (
"net/http"
"github.com/casbin/casbin/v2"
"github.com/gin-gonic/gin"
)
func CasbinAuth(e *casbin.Enforcer) gin.HandlerFunc {
return func(c *gin.Context) {
// 从 JWT / Session 中获取用户(此处简化)
user := c.GetString("username")
if user == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "未登录"})
return
}
obj := c.Request.URL.Path
act := c.Request.Method
ok, err := e.Enforce(user, obj, act)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "鉴权失败"})
return
}
if !ok {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "无权限"})
return
}
c.Next()
}
}
14.3 路由注册
go
package main
import (
"github.com/casbin/casbin/v2"
"github.com/gin-gonic/gin"
"casbin-demo/middleware"
)
func main() {
e, _ := casbin.NewEnforcer("model_rbac.conf", "policy_rbac.csv")
r := gin.Default()
// 模拟登录后设置 username
r.Use(func(c *gin.Context) {
c.Set("username", "alice") // 实际从 token 解析
c.Next()
})
api := r.Group("/api", middleware.CasbinAuth(e))
{
api.GET("/users", listUsers)
api.POST("/users", createUser)
api.DELETE("/users/:id", deleteUser)
}
r.Run(":8080")
}
func listUsers(c *gin.Context) { c.JSON(200, gin.H{"msg": "用户列表"}) }
func createUser(c *gin.Context) { c.JSON(200, gin.H{"msg": "创建用户"}) }
func deleteUser(c *gin.Context) { c.JSON(200, gin.H{"msg": "删除用户"}) }
14.4 集成思路总结
HTTP 请求
↓
认证中间件(JWT)→ 得到 user
↓
Casbin 中间件 → Enforce(user, path, method)
↓
业务 Handler
15. 最佳实践与常见坑
15.1 最佳实践
-
Model 稳定,Policy 灵活
Model 定义后尽量少改;权限变更只改 Policy 或数据库记录。
-
用角色而非直接给用户配权限
用户量大时,
g关系比大量p规则更易维护。 -
路径匹配选对函数
- 简单前缀:
keyMatch - RESTful 带参数:
keyMatch2 - 复杂规则:
regexMatch
- 简单前缀:
-
生产环境用 DB Adapter
文件 Adapter 适合开发;线上需要动态增删权限、多实例部署。
-
Enforcer 单例
Enforcer 创建有开销,应用启动时创建一次,全局复用。
-
修改策略后 SavePolicy
动态修改后调用
SavePolicy()持久化,否则重启丢失。
15.2 常见坑
| 问题 | 原因 | 解决 |
|---|---|---|
| Enforce 始终 false | matcher 写错或 policy 无匹配 | e.EnableLog(true) 看日志 |
| 改 policy 不生效 | 内存未 reload | e.LoadPolicy() |
| 参数顺序错误 | Enforce 参数与 request_definition 不一致 | 严格对照 model |
| 角色继承不生效 | role_definition 未定义或 matcher 未用 g() | 检查 model |
| ABAC 结构体字段访问失败 | 字段需导出(大写开头) | Age 而非 age |
| 性能问题 | 每次请求 NewEnforcer | 使用单例 + 缓存 |
15.3 调试技巧
go
e.EnableLog(true) // 打印 matcher 匹配过程
// 查看当前加载的全部策略
policies, _ := e.GetPolicy()
for _, p := range policies {
fmt.Println(p)
}
16. 动手练习
建议按顺序完成,巩固本章内容。
练习 1:基础 ACL
- 创建 ACL 模型和策略
- 实现:
admin对report有 read/write;guest对report只有 read - 用 Go 写测试用例验证
参考答案(点击展开)
policy.csv
csv
p, admin, report, read
p, admin, report, write
p, guest, report, read
main.go 片段
go
e, _ := casbin.NewEnforcer("model.conf", "policy.csv")
fmt.Println(e.Enforce("admin", "report", "write")) // true
fmt.Println(e.Enforce("guest", "report", "write")) // false
练习 2:RBAC 角色继承
- 定义角色:
superadmin>admin>user superadmin可 DELETE/api/*admin可 POST/api/*user可 GET/api/*- 验证 alice 是 admin 时,不能 DELETE(未继承 superadmin)
参考答案(点击展开)
policy.csv
csv
p, superadmin, /api/*, DELETE
p, admin, /api/*, POST
p, user, /api/*, GET
g, superadmin, admin
g, admin, user
g, alice, admin
model.conf matcher
ini
m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && r.act == p.act
go
e.Enforce("alice", "/api/users", "POST") // true
e.Enforce("alice", "/api/users", "DELETE") // false
练习 3:多租户
- 租户
companyA和companyB - 同一用户
tom在 A 是 admin,在 B 是 viewer - 实现 domain 模型并测试
提示
使用 [8. RBAC with Domains](#8. RBAC with Domains) 的模型,Enforce 四个参数:(user, domain, obj, act)。
练习 4:Gin 集成
- 搭建 Gin 服务
- 接入 Casbin 中间件
- 用 curl 测试有权限和无权限的请求
bash
# 有权限(alice + GET /api/users)
curl -i http://localhost:8080/api/users
# 可在中间件里临时把 user 改成 bob 测试 403
17. 参考资源
| 资源 | 链接 |
|---|---|
| Casbin 官网 | https://casbin.org/ |
| Casbin Go 文档 | https://casbin.org/docs/overview |
| Model 语法 | https://casbin.org/docs/syntax-for-models |
| Policy 语法 | https://casbin.org/docs/syntax-for-policy |
| 在线编辑器 | https://casbin.org/editor/ |
| GitHub | https://github.com/casbin/casbin |
附录 A:完整可运行 RBAC 示例项目
将以下内容保存后可直接 go run main.go。
目录结构
casbin-rbac-demo/
├── go.mod
├── main.go
├── model.conf
└── policy.csv
go.mod
go
module casbin-rbac-demo
go 1.21
require github.com/casbin/casbin/v2 v2.100.0
model.conf
ini
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && keyMatch2(r.obj, p.obj) && regexMatch(r.act, p.act)
policy.csv
csv
p, admin, /api/users, (GET|POST|PUT|DELETE)
p, editor, /api/articles, (GET|POST)
p, viewer, /api/articles, GET
g, alice, admin
g, bob, editor
g, charlie, viewer
main.go
go
package main
import (
"fmt"
"log"
"github.com/casbin/casbin/v2"
)
func main() {
e, err := casbin.NewEnforcer("model.conf", "policy.csv")
if err != nil {
log.Fatal(err)
}
type testCase struct {
user, path, method string
expect bool
}
cases := []testCase{
{"alice", "/api/users", "DELETE", true},
{"alice", "/api/users/1", "GET", true},
{"bob", "/api/articles", "POST", true},
{"bob", "/api/users", "GET", false},
{"charlie", "/api/articles", "GET", true},
{"charlie", "/api/articles", "POST", false},
}
fmt.Println("=== Casbin RBAC 鉴权测试 ===")
for _, c := range cases {
ok, err := e.Enforce(c.user, c.path, c.method)
if err != nil {
log.Printf("鉴权出错: %v", err)
continue
}
mark := "PASS"
if ok != c.expect {
mark = "FAIL"
}
fmt.Printf("[%s] %s %s %s => %v (期望 %v)\n", mark, c.user, c.method, c.path, ok, c.expect)
}
fmt.Println("\n=== 动态添加权限 ===")
_, _ = e.AddRoleForUser("david", "viewer")
ok, _ := e.Enforce("david", "/api/articles", "GET")
fmt.Println("david GET /api/articles:", ok) // true
_, _ = e.AddPolicy("viewer", "/api/profile", "GET")
ok, _ = e.Enforce("charlie", "/api/profile", "GET")
fmt.Println("charlie GET /api/profile:", ok) // true
}