匹配器与函数
matcher 表达式的表达力来自内置匹配函数。本章给出 keyMatch 全系列的行为对照表(最容易混淆的部分)、regexMatch/ipMatch/globMatch 用法、自定义函数注册,以及 eval() 动态规则进阶。
内置函数总览
| 函数 | 用途 | 典型 pattern |
|---|---|---|
keyMatch(key, pattern) |
URL 通配,* 匹配任意(含 /) |
/foo/* |
keyMatch2(key, pattern) |
RESTful 风格,:param + * |
/foo/:id |
keyMatch3(key, pattern) |
同 keyMatch2,参数用 {} |
/foo/{id} |
keyMatch4(key, pattern) |
{param} 且同名参数必须同值 |
/{id}/x/{id} |
keyMatch5(key, pattern) |
匹配前忽略 ? 后的 query string |
/foo/* |
keyGet(key, pattern) |
返回 * 匹配到的部分 |
/foo/* |
keyGet2(key, pattern, name) |
返回 :name 参数匹配到的值 |
/foo/:id |
regexMatch(key, pattern) |
正则匹配 | (GET)|(POST) |
ipMatch(ip, cidr) |
IP / CIDR 匹配 | 192.168.2.0/24 |
globMatch(key, pattern) |
shell glob 语义 | /foo/* |
keyMatch 系列行为对照(🚨 高频踩坑区)
| 表达式 | 结果 | 说明 |
|---|---|---|
keyMatch("/foo/bar", "/foo/*") |
✅ | * 匹配一切,包括 / |
keyMatch("/foo/a/b/c", "/foo/*") |
✅ | 跨多级 |
keyMatch("/foo", "/foo/*") |
❌ | 没有 / 后缀不匹配(注意!) |
keyMatch2("/foo/42", "/foo/:id") |
✅ | :id 匹配单段 |
keyMatch2("/foo/42/edit", "/foo/:id") |
❌ | :id 不跨 / |
keyMatch2("/foo/42/edit", "/foo/:id/edit") |
✅ | |
keyMatch2("/foo/a/b", "/foo/*") |
✅ | * 在 keyMatch2 中同样跨级 |
keyMatch3("/foo/42", "/foo/{id}") |
✅ | 与 keyMatch2 相同,仅参数记法不同 |
keyMatch4("/1/x/1", "/{id}/x/{id}") |
✅ | 同名参数值一致 |
keyMatch4("/1/x/2", "/{id}/x/{id}") |
❌ | 同名参数值不同 → 拒绝 |
keyMatch5("/foo/1?page=2", "/foo/*") |
✅ | 先剥掉 ?page=2 再匹配 |
keyMatch("/foo/1?page=2", "/foo/*") |
✅(但含 query) | query 被当作路径一部分参与匹配 |
💡 最佳实践:保护 REST API 用 keyMatch2(:id 记法与 Gin 路由一致);请求 URL 可能带 query string 时,要么在中间件里传 c.Request.URL.Path(不含 query,推荐),要么用 keyMatch5。
keyGet:提取路径参数参与判定
[matchers]
m = r.sub == keyGet2(r.obj, "/users/:uid/profile", "uid")e.Enforce("alice", "/users/alice/profile") // true:路径里的 uid 就是自己
e.Enforce("alice", "/users/bob/profile") // false无需任何策略行就实现了"只能看自己的主页"——keyGet 把 ABAC 思路带进了 URL 匹配。
regexMatch
完整 Go 正则语法(RE2):
m = r.sub == p.sub && keyMatch2(r.obj, p.obj) && regexMatch(r.act, p.act)p, alice, /api/articles/*, "(GET)|(POST)"
p, bob, /api/*, ".*"🚨 陷阱:regexMatch 是部分匹配(regexp.MatchString 语义),regexMatch("DELETE", "ELET") 也是 true。严格匹配请加锚点:"^(GET)|(POST)$" 写法也有坑(| 优先级),正确写法是 "^(GET|POST)$"。
ipMatch
限制来源 IP 的模型:
[request_definition]
r = sub, obj, act, ip
[policy_definition]
p = sub, obj, act, ip
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act && ipMatch(r.ip, p.ip)p, admin, /internal/*, ".*", 10.0.0.0/8
p, alice, /api/*, GET, 192.168.2.15e.Enforce("admin", "/internal/metrics", "GET", "10.1.2.3") // true:命中 CIDRglobMatch
shell glob 语义(* 不跨 /):
globMatch("/foo/bar", "/foo/*") // true
globMatch("/foo/a/b", "/foo/*") // false —— 与 keyMatch 关键区别需要"只匹配一级子路径"时 globMatch 比 keyMatch 更合适。
自定义函数
内置函数不够用时(如需要大小写不敏感匹配、业务编码规则),注册自己的函数:
import "github.com/casbin/casbin/v3/util"
// 1. 业务函数:判断资源编码是否属于某个前缀域
func prefixMatch(key string, prefix string) bool {
return strings.HasPrefix(key, prefix+":")
}
// 2. 包装为 govaluate 函数签名
func PrefixMatchFunc(args ...interface{}) (interface{}, error) {
if err := util.ValidateVariadicArgs(2, args...); err != nil {
return false, fmt.Errorf("prefixMatch: %w", err)
}
key := args[0].(string)
prefix := args[1].(string)
return prefixMatch(key, prefix), nil
}
// 3. 注册到 enforcer(必须在 Enforce 之前)
e.AddFunction("prefixMatch", PrefixMatchFunc)[matchers]
m = g(r.sub, p.sub) && prefixMatch(r.obj, p.obj) && r.act == p.actp, admin, order, write # admin 可写所有 order:xxx 资源e.Enforce("alice", "order:20260717001", "write") // true🚨 陷阱:自定义函数内的 panic(如类型断言失败)会导致 Enforce 崩溃。参数先 ValidateVariadicArgs 校验个数,再用 v, ok := args[0].(string) 安全断言。
⚡ 性能提示:matcher 对每条策略都执行一次,自定义函数里不要做 IO(查库、调 RPC)。需要外部数据时,在调用 Enforce 前查好、作为请求参数传入(ABAC 思路)。
eval() 动态规则进阶
02-访问控制模型大全 介绍了基本用法:策略字段存表达式,matcher 用 eval() 执行。进阶要点:
多个 eval 字段
[policy_definition]
p = sub_rule, obj_rule, act
[matchers]
m = eval(p.sub_rule) && eval(p.obj_rule) && r.act == p.actp, r.sub.Level >= 3, r.obj.Public == true, readeval 与 RBAC 混用:按角色挂动态规则
[policy_definition]
p = sub, sub_rule, obj, act
[role_definition]
g = _, _
[matchers]
m = g(r.sub.Name, p.sub) && eval(p.sub_rule) && keyMatch2(r.obj, p.obj) && r.act == p.actp, auditor, r.sub.Department == "finance", /reports/:id, GET
g, alice, auditoralice 是 auditor 且部门是 finance 才能看报表——静态角色与动态属性双闸门。
注意此模型中 r.sub 是结构体:g() 用 r.sub.Name,eval 规则用 r.sub.Department。
type Subject struct {
Name string
Department string
}
e.Enforce(Subject{Name: "alice", Department: "finance"}, "/reports/1", "GET")🔬 深入原理:eval 的实现与代价
Enforce 时 Casbin 检测 matcher 含 eval(p.x),会把该策略字段的文本替换进 matcher 再重新解析表达式。因此:
- 每条 eval 策略每次判定都要重新编译表达式——比普通 matcher 慢一个数量级,eval 策略应控制数量
- 策略内容成为可执行逻辑——写策略的权限 = 写规则引擎代码的权限。策略管理接口必须校验表达式合法性(可用
govaluate.NewEvaluableExpression预检)并限制谁能写 eval 类策略
常见陷阱
🚨 keyMatch 与 keyMatch2 的 * 语义混记:两者的 * 都跨 /,但 :id / {id} 只匹配单段。"/api/* 挡不住 /api/admin/delete“不是 bug,是 * 本来就跨级——想只放一级,用 globMatch。
🚨 /foo 不匹配 /foo/*:给资源授权 /api/users/* 后,访问 /api/users(无尾斜杠)是 false。通常要授两条:/api/users 和 /api/users/*,或用 regexMatch 写 ^/api/users(/.*)?$。
🚨 regexMatch 部分匹配:不加 ^...$ 锚点时子串命中即通过,HTTP 方法匹配务必写 "^(GET|POST)$"。
🚨 query string 混入匹配:中间件里传了 c.Request.RequestURI(带 ?a=b)导致 keyMatch2 失败。传 c.Request.URL.Path。
🚨 函数名大小写:matcher 中函数名与 AddFunction 注册名必须完全一致,写错报 “No parameter ‘xxxMatch’ found”。
🚨 在自定义函数里做 IO:策略 1 万条 = 每次 Enforce 调 1 万次函数。函数必须是纯内存计算。