Skip to content
Go
匹配器与函数

匹配器与函数

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.15
e.Enforce("admin", "/internal/metrics", "GET", "10.1.2.3") // true:命中 CIDR

globMatch

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.act
p, 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.act
p, r.sub.Level >= 3, r.obj.Public == true, read

eval 与 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.act
p, auditor, r.sub.Department == "finance", /reports/:id, GET
g, alice, auditor

alice 是 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 再重新解析表达式。因此:

  1. 每条 eval 策略每次判定都要重新编译表达式——比普通 matcher 慢一个数量级,eval 策略应控制数量
  2. 策略内容成为可执行逻辑——写策略的权限 = 写规则引擎代码的权限。策略管理接口必须校验表达式合法性(可用 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 万次函数。函数必须是纯内存计算。