对于使用教程就不多说了,文档中已经写的很详细了,今天主要关注 casbin 的内部实现。
从使用方式上来看,casbin 最重要的两个函数一个是 NewEnforced() 还有 Enforced() 两个函数,一个是负责对 model.conf 还有 policy 进行读取,从而生成自己的 model,另一个是将 model 与 rvals 进行对比,得出想要的结论
Demo
model.conf
# Request definition
[request_definition]
r = sub, obj, act
# Policy definition
[policy_definition]
p = sub, obj, act
# Policy effect
[policy_effect]
e = some(where (p.eft == allow))
# Matchers
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
policy.csv
p, alice, data1, read
p, bob, data2, write
main.go
package main
import (
"fmt"
"log"
"github.com/casbin/casbin"
)
func main() {
// setup casbin auth rules
e, err := casbin.NewEnforcer("./model.conf", "./policy.csv")
if err != nil {
log.Fatal(err)
}
ok, err := e.Enforce("alice", "data1", "read")
if ok && err == nil {
fmt.Print("Success")
} else {
fmt.Print("Fail")
}
}
Model
在实例中,我们使用 “./model.conf” 和 “policy.csv” 生成 Model,这个结构体是我们进行对比时的核心,在 model.conf 文件中,我们指定请求参数还有策略的形式,还可以声明 g 函数,可以用来绑定实体之间的关系,这一部分只能通过 model.conf 文件来进行指定。对于真正的策略信息,是在 policy 中的,通过 Adapter 接口,我们可以从不同的地方读取到这一内容。,官方提供的工具这里真的是相当之多了,基本上大部分的需求都能够满足。
func (model Model) loadModelFromConfig(cfg config.ConfigInterface) error {
for s := range sectionNameMap {
loadSection(model, cfg, s)
}
// 检验是否全部需要的 section 都存在
// ...
}
func loadSection(model Model, cfg config.ConfigInterface, sec string) {
// 处理 r, r1 的情况
i := 1
for {
if !loadAssertion(model, cfg, sec, sec+getKeySuffix(i)) {
break
} else {
i++
}
}
}
// 这里 sec 是 "r", "p", "e", "m","g",而 key 是 “r”, “r1”
func loadAssertion(model Model, cfg config.ConfigInterface, sec string, key string) bool {
value := cfg.String(sectionNameMap[sec] + "::" + key)
fmt.Println(value)
return model.AddDef(sec, key, value)
}
Model 其实是两个连续的 map,对于两个键值的关系,很多时候我们会看到 model[r][r] 的内容,这是因为我们将整个 model 文件看作是由 section 组成的,而没一个 section 中,能够多个表达式,也就是 r,r1 的样子,所以我们需要两层的 map 支持这一功能。
每一个表达式最后会转化为一个 Assertion,每个 Assertion 主要的内容是两个切片,一个是 Tokens 存储我们的 model 内容,比如说如果 r = sub, obj, act
那么最后 Tokens 中会是 ["r_sub","r_obj","r_act"]
而 Policy 中就是策略的内容了,如果是文件的话,其实就是对于每一行都执行这个函数,如果是空或者 # 开头就会被忽略,否则附加到 Policy 后面。
func LoadPolicyLine(line string, model model.Model) {
if line == "" || strings.HasPrefix(line, "#") {
return
}
tokens := strings.Split(line, ",")
for i := 0; i < len(tokens); i++ {
tokens[i] = strings.TrimSpace(tokens[i])
}
key := tokens[0]
sec := key[:1]
model[sec][key].Policy = append(model[sec][key].Policy, tokens[1:])
}
在初始化中还有一个很重要的点是 FunctionMap,这是实现模式匹配的重点,这些函数存储在 ‘util/builtin_opertion.go’ 文件中,在注释中这些函数的作用已经很清楚了,因为对于表达式的处理,casbin 使用的是第三方库 govaluate,如果想要真正弄懂这些函数到底是怎么运作的,还需要去读这个库的源码
enforce()
这个函数应该是这个项目中最长的一个函数了,它的内容也相对来说复杂一点。这个函数内的核心操作其实就是使用 macther 生成表达式,并且将数据传入进行验证,然后验证的结果再进行整理。
function
这里使用的主要函数是 Knetic/govaluate,这里只用到其中一个函数,介绍中是这样的,我们在 matcher 中的函数可以调用我们传递进入的 functionMap 中的函数。
functions := map[string]govaluate.ExpressionFunction {
"strlen": func(args ...interface{}) (interface{}, error) {
length := len(args[0].(string))
return (float64)(length), nil
},
}
expString := "strlen('someReallyLongInputString') <= 16"
expression, _ := govaluate.NewEvaluableExpressionWithFunctions(expString, functions)
result, _ := expression.Evaluate(nil)
parameters
传入到表达式的参数,定义在下面这个结构体中,在一次检测中 rTokens
, rVals
, pTokens
都是不变的,需要变得是 pVals 中的参数,主要是让 pVals 与 rVals 进行对比,有多少 Policy 就要对比到少次,也就产生多少结果。所以这个时候就需要 effect 来处理结果了。
type enforceParameters struct {
rTokens map[string]int
rVals []interface{}
pTokens map[string]int
pVals []string
}
最后产生的结果,是一个 interface{}
类型,有两种映射的可能 ,bool 和 float64,真的想象不到到底什么情况下会出现 float64 的情况,这个第三方库也提供计算功能吗?
effect
结果是 int 型的
type Effect int
const (
Allow Effect = iota
Indeterminate
Deny
)
其中一共有四种合并策略,这个实现也非常简单,仅仅只是字符串的匹配,本来还以为这是通过解析器做的
some(where (p.eft == allow)) | 有一个允许,就允许 |
---|---|
!some(where (p.eft == deny)) | 全部允许才允许 |
some(where (p.eft == allow)) && !some(where (p.eft == deny)) | 存在允许,切不存在拒绝 |
priority(p.eft) || deny | 选择第一个确定的结果 |