快捷搜索:  汽车  科技

goroutine 状态切换:Go每日一库之casbin

goroutine 状态切换:Go每日一库之casbineffect根据对请求运用匹配器得出的所有结果进行汇总,来决定该请求是允许还是拒绝。matcher匹配器会将请求与定义的每个policy一一匹配,生成多个匹配结果。$gogetgithub.com/casbin/casbin/v2权限实际上就是控制谁能对什么资源进行什么操作。casbin将访问控制模型抽象到一个基于 PERM(Policy,Effect,Request,Matchers) 元模型的配置文件(模型文件)中。因此切换或更新授权机制只需要简单地修改配置文件。policy是策略或者说是规则的定义。它定义了具体的规则。request是对访问请求的抽象,它与e.Enforce()函数的参数是一一对应的

简介

权限管理在几乎每个系统中都是必备的模块。如果项目开发每次都要实现一次权限管理,无疑会浪费开发时间,增加开发成本。因此,casbin库出现了。casbin是一个强大、高效的访问控制库。支持常用的多种访问控制模型,如ACL/RBAC/ABAC等。可以实现灵活的访问权限控制。同时,casbin支持多种编程语言,Go/Java/Node/PHP/Python/.NET/Rust。我们只需要一次学习,多处运用

快速使用

我们依然使用 Go Module 编写代码,先初始化:

$ mkdir casbin && cd casbin $ go mod init GitHub.com/darjun/go-daily-lib/casbin

然后安装casbin,目前是v2版本:

$gogetgithub.com/casbin/casbin/v2

权限实际上就是控制能对什么资源进行什么操作。casbin将访问控制模型抽象到一个基于 PERM(Policy,Effect,Request,Matchers) 元模型的配置文件(模型文件)中。因此切换或更新授权机制只需要简单地修改配置文件。

policy是策略或者说是规则的定义。它定义了具体的规则。

request是对访问请求的抽象,它与e.Enforce()函数的参数是一一对应的

matcher匹配器会将请求与定义的每个policy一一匹配,生成多个匹配结果。

effect根据对请求运用匹配器得出的所有结果进行汇总,来决定该请求是允许还是拒绝

下面这张图很好地描绘了这个过程:

goroutine 状态切换:Go每日一库之casbin(1)

我们首先编写模型文件:

[request_definition] r = sub obj act [policy_definition] p = sub obj act [matchers] m = r.sub == p.sub && r.obj == p.obj && r.act == p.act [policy_effect] e = some(where (p.eft == allow))

上面模型文件规定了权限由sub obj act三要素组成,只有在策略列表中有和它完全相同的策略时,该请求才能通过。匹配器的结果可以通过p.eft获取,some(where (p.eft == allow))表示只要有一条策略允许即可。

然后我们策略文件(即谁能对什么资源进行什么操作):

p dajun data1 read p lizi data2 write

上面policy.csv文件的两行内容表示dajun对数据data1有read权限,lizi对数据data2有write权限。

接下来就是使用的代码:

packagemain import( "fmt" "log" "github.com/casbin/casbin/v2" ) funccheck(e*casbin.Enforcer sub obj actstring){ ok _:=e.Enforce(sub obj act) ifok{ fmt.Printf("%sCAN%s%s\n" sub act obj) }else{ fmt.Printf("%sCANNOT%s%s\n" sub act obj) } } funcmain(){ e err:=casbin.NewEnforcer("./model.conf" "./policy.csv") iferr!=nil{ log.Fatalf("NewEnforecerfailed:%v\n" err) } check(e "dajun" "data1" "read") check(e "lizi" "data2" "write") check(e "dajun" "data1" "write") check(e "dajun" "data2" "read") }

代码其实不复杂。首先创建一个casbin.Enforcer对象,加载模型文件model.conf和策略文件policy.csv,调用其Enforce方法来检查权限。运行程序:

$ go run main.go dajun CAN read data1 lizi CAN write data2 dajun CANNOT write data1 dajun CANNOT read data2

请求必须完全匹配某条策略才能通过。("dajun" "data1" "read")匹配p dajun data1 read,("lizi" "data2" "write")匹配p lizi data2 write,所以前两个检查通过。第 3 个因为"dajun"没有对data1的write权限,第 4 个因为dajun对data2没有read权限,所以检查都不能通过。输出结果符合预期。

sub/obj/act依次对应传给Enforce方法的三个参数。实际上这里的sub/obj/act和read/write/data1/data2是我自己随便取的,你完全可以使用其它的名字,只要能前后一致即可。

上面例子中实现的就是ACL(access-control-list,访问控制列表)。ACL显示定义了每个主体对每个资源的权限情况,未定义的就没有权限。我们还可以加上超级管理员,超级管理员可以进行任何操作。假设超级管理员为root,我们只需要修改匹配器:

[matchers] e = r.sub == p.sub && r.obj == p.obj && r.act == p.act || r.sub == "root"

只要访问主体是root一律放行。

验证:

funcmain(){ e err:=casbin.NewEnforcer("./model.conf" "./policy.csv") iferr!=nil{ log.Fatalf("NewEnforecerfailed:%v\n" err) } check(e "root" "data1" "read") check(e "root" "data2" "write") check(e "root" "data1" "execute") check(e "root" "data3" "rwx") }

因为sub = "root"时,匹配器一定能通过,运行结果:

$ go run main.go root CAN read data1 root CAN write data2 root CAN execute data1 root CAN rwx data3

RBAC 模型

ACL模型在用户和资源都比较少的情况下没什么问题,但是用户和资源量一大,ACL就会变得异常繁琐。想象一下,每次新增一个用户,都要把他需要的权限重新设置一遍是多么地痛苦。RBAC(role-based-access-control)模型通过引入角色(role)这个中间层来解决这个问题。每个用户都属于一个角色,例如开发者、管理员、运维等,每个角色都有其特定的权限,权限的增加和删除都通过角色来进行。这样新增一个用户时,我们只需要给他指派一个角色,他就能拥有该角色的所有权限。修改角色的权限时,属于这个角色的用户权限就会相应的修改。

在casbin中使用RBAC模型需要在模型文件中添加role_definition模块:

[role_definition] g = _ _ [matchers] m = g(r.sub p.sub) && r.obj == p.obj && r.act == p.act

g = _ _定义了用户——角色,角色——角色的映射关系,前者是后者的成员,拥有后者的权限。然后在匹配器中,我们不需要判断r.sub与p.sub完全相等,只需要使用g(r.sub p.sub)来判断请求主体r.sub是否属于p.sub这个角色即可。最后我们修改策略文件添加用户——角色定义:

p admin data read p admin data write p developer data read g dajun admin g lizi developer

上面的policy.csv文件规定了,dajun属于admin管理员,lizi属于developer开发者,使用g来定义这层关系。另外admin对数据data用read和write权限,而developer对数据data只有read权限。

packagemain import( "fmt" "log" "github.com/casbin/casbin/v2" ) funccheck(e*casbin.Enforcer sub obj actstring){ ok _:=e.Enforce(sub obj act) ifok{ fmt.Printf("%sCAN%s%s\n" sub act obj) }else{ fmt.Printf("%sCANNOT%s%s\n" sub act obj) } } funcmain(){ e err:=casbin.NewEnforcer("./model.conf" "./policy.csv") iferr!=nil{ log.Fatalf("NewEnforecerfailed:%v\n" err) } check(e "dajun" "data" "read") check(e "dajun" "data" "write") check(e "lizi" "data" "read") check(e "lizi" "data" "write") }

很显然lizi所属角色没有write权限:

dajun CAN read data dajun CAN write data lizi CAN read data lizi CANNOT write data

多个RBAC

casbin支持同时存在多个RBAC系统,即用户和资源都有角色:

[role_definition] g=_ _ g2=_ _ [matchers] m = g(r.sub p.sub) && g2(r.obj p.obj) && r.act == p.act

上面的模型文件定义了两个RBAC系统g和g2,我们在匹配器中使用g(r.sub p.sub)判断请求主体属于特定组,g2(r.obj p.obj)判断请求资源属于特定组,且操作一致即可放行。

策略文件:

p admin prod read p admin prod write p admin dev read p admin dev write p developer dev read p developer dev write p developer prod read g dajun admin g lizi developer g2 prod.data prod g2 dev.data dev

先看角色关系,即最后 4 行,dajun属于admin角色,lizi属于developer角色,prod.data属于生产资源prod角色,dev.data属于开发资源dev角色。admin角色拥有对prod和dev类资源的读写权限,developer只能拥有对dev的读写权限和prod的读权限。

check(e "dajun" "prod.data" "read") check(e "dajun" "prod.data" "write") check(e "lizi" "dev.data" "read") check(e "lizi" "dev.data" "write") check(e "lizi" "prod.data" "write")

第一个函数中e.Enforce()方法在实际执行的时候先获取dajun所属角色admin,再获取prod.data所属角色prod,根据文件中第一行p admin prod read允许请求。最后一个函数中lizi属于角色developer,而prod.data属于角色prod,所有策略都不允许,故该请求被拒绝:

dajun CAN read prod.data dajun CAN write prod.data lizi CAN read dev.data lizi CAN write dev.data lizi CANNOT write prod.data

多层角色

casbin还能为角色定义所属角色,从而实现多层角色关系,这种权限关系是可以传递的。例如dajun属于高级开发者senior,seinor属于开发者,那么dajun也属于开发者,拥有开发者的所有权限。我们可以定义开发者共有的权限,然后额外为senior定义一些特殊的权限。

模型文件不用修改,策略文件改动如下:

p senior data write p developer data read g dajun senior g senior developer g lizi developer

上面policy.csv文件定义了高级开发者senior对数据data有write权限,普通开发者developer对数据只有read权限。同时senior也是developer,所以senior也继承其read权限。dajun属于senior,所以dajun对data有read和write权限,而lizi只属于developer,对数据data只有read权限。

check(e "dajun" "data" "read") check(e "dajun" "data" "write") check(e "lizi" "data" "read") check(e "lizi" "data" "write")

RBAC domain

在casbin中,角色可以是全局的,也可以是特定domain(领域)或tenant(租户),可以简单理解为。例如dajun在组tenant1中是管理员,拥有比较高的权限,在tenant2可能只是个弟弟。

使用RBAC domain需要对模型文件做以下修改:

[request_definition] r = sub dom obj act [policy_definition] p = sub dom obj act [role_definition] g = _ _ _ [matchers] m = g(r.sub p.sub r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.obj

g=_ _ _表示前者在后者中拥有中间定义的角色,在匹配器中使用g要带上dom。

p admin tenant1 data1 read p admin tenant2 data2 read g dajun admin tenant1 g dajun developer tenant2

在tenant1中,只有admin可以读取数据data1。在tenant2中,只有admin可以读取数据data2。dajun在tenant1中是admin,但是在tenant2中不是。

funccheck(e*casbin.Enforcer sub domain obj actstring){ ok _:=e.Enforce(sub domain obj act) ifok{ fmt.Printf("%sCAN%s%sin%s\n" sub act obj domain) }else{ fmt.Printf("%sCANNOT%s%sin%s\n" sub act obj domain) } } funcmain(){ e err:=casbin.NewEnforcer("./model.conf" "./policy.csv") iferr!=nil{ log.Fatalf("NewEnforecerfailed:%v\n" err) } check(e "dajun" "tenant1" "data1" "read") check(e "dajun" "tenant2" "data2" "read") }

结果不出意料:

dajun CAN read data1 in tenant1 dajun CANNOT read data2 in tenant2

ABAC

RBAC模型对于实现比较规则的、相对静态的权限管理非常有用。但是对于特殊的、动态的需求,RBAC就显得有点力不从心了。例如,我们在不同的时间段对数据data实现不同的权限控制。正常工作时间9:00-18:00所有人都可以读写data,其他时间只有数据所有者能读写。这种需求我们可以很方便地使用ABAC(attribute base access list)模型完成:

[request_definition] r = sub obj act [policy_definition] p = sub obj act [matchers] m = r.sub.Hour >= 9 && r.sub.Hour < 18 || r.sub.Name == r.obj.Owner [policy_effect] e = some(where (p.eft == allow))

该规则不需要策略文件:

typeObjectstruct{ Namestring Ownerstring } typeSubjectstruct{ Namestring Hourint } funccheck(e*casbin.Enforcer subSubject objObject actstring){ ok _:=e.Enforce(sub obj act) ifok{ fmt.Printf("%sCAN%s%sat%d:00\n" sub.Name act obj.Name sub.Hour) }else{ fmt.Printf("%sCANNOT%s%sat%d:00\n" sub.Name act obj.Name sub.Hour) } } funcmain(){ e err:=casbin.NewEnforcer("./model.conf" "./policy.csv") iferr!=nil{ log.Fatalf("NewEnforecerfailed:%v\n" err) } o:=Object{"data" "dajun"} s1:=Subject{"dajun" 10} check(e s1 o "read") s2:=Subject{"lizi" 10} check(e s2 o "read") s3:=Subject{"dajun" 20} check(e s3 o "read") s4:=Subject{"lizi" 20} check(e s4 o "read") }

显然lizi在20:00不能read数据data:

dajun CAN read data at 10:00 lizi CAN read data at 10:00 dajun CAN read data at 20:00 lizi CANNOT read data at 20:00

我们知道,在model.conf文件中可以通过r.sub和r.obj,r.act来访问传给Enforce方法的参数。实际上sub/obj可以是结构体对象,得益于govaluate库的强大功能,我们可以在model.conf文件中获取这些结构体的字段值。如上面的r.sub.Name、r.Obj.Owner等。govaluate库的内容可以参见我之前的一篇文章《Go 每日一库之 govaluate》。

使用ABAC模型可以非常灵活的权限控制,但是一般情况下RBAC就已经够用了。

模型存储

上面代码中,我们一直将模型存储在文件中。casbin也可以实现在代码中动态初始化模型,例如get-started的例子可以改写为:

funcmain(){ m:=model.NewModel() m.AddDef("r" "r" "sub obj act") m.AddDef("p" "p" "sub obj act") m.AddDef("e" "e" "some(where(p.eft==allow))") m.AddDef("m" "m" "r.sub==g.sub&&r.obj==p.obj&&r.act==p.act") a:=fileadapter.NewAdapter("./policy.csv") e err:=casbin.NewEnforcer(m a) iferr!=nil{ log.Fatalf("NewEnforecerfailed:%v\n" err) } check(e "dajun" "data1" "read") check(e "lizi" "data2" "write") check(e "dajun" "data1" "write") check(e "dajun" "data2" "read") }

同样地,我们也可以从字符串中加载模型:

funcmain(){ text:=` [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 ` m _:=model.NewModelFromString(text) a:=fileadapter.NewAdapter("./policy.csv") e _:=casbin.NewEnforcer(m a) check(e "dajun" "data1" "read") check(e "lizi" "data2" "write") check(e "dajun" "data1" "write") check(e "dajun" "data2" "read") }

但是这两种方式并不推荐。

策略存储

在前面的例子中,我们都是将策略存储在policy.csv文件中。一般在实际应用中,很少使用文件存储。casbin以第三方适配器的方式支持多种存储方式包括MySQL/MongoDB/Redis/Etcd等,还可以实现自己的存储。完整列表看这里https://casbin.org/docs/en/adapters。下面我们介绍使用Gorm Adapter。先连接到数据库,执行下面的SQL:

CREATEDATABASEIFNOTEXISTScasbin; USEcasbin; CREATETABLEIFNOTEXISTScasbin_rule( p_typeVARCHAR(100)NOTNULL v0VARCHAR(100) v1VARCHAR(100) v2VARCHAR(100) v3VARCHAR(100) v4VARCHAR(100) v5VARCHAR(100) ); INSERTINTOcasbin_ruleVALUES ('p' 'dajun' 'data1' 'read' '' '' '') ('p' 'lizi' 'data2' 'write' '' '' '');

然后使用Gorm Adapter加载policy,Gorm Adapter默认使用casbin库中的casbin_rule表:

packagemain import( "fmt" "github.com/casbin/casbin/v2" gormadapter"github.com/casbin/gorm-adapter/v2" _"github.com/go-sql-driver/mysql" ) funccheck(e*casbin.Enforcer sub obj actstring){ ok _:=e.Enforce(sub obj act) ifok{ fmt.Printf("%sCAN%s%s\n" sub act obj) }else{ fmt.Printf("%sCANNOT%s%s\n" sub act obj) } } funcmain(){ a _:=gormadapter.NewAdapter("mysql" "root:12345@tcp(127.0.0.1:3306)/") e _:=casbin.NewEnforcer("./model.conf" a) check(e "dajun" "data1" "read") check(e "lizi" "data2" "write") check(e "dajun" "data1" "write") check(e "dajun" "data2" "read") }

运行:

dajun CAN read data1 lizi CAN write data2 dajun CANNOT write data1 dajun CANNOT read data2

使用函数

我们可以在匹配器中使用函数。casbin内置了一些函数keyMatch/keyMatch2/keyMatch3/keyMatch4都是匹配 URL 路径的,regexMatch使用正则匹配,ipMatch匹配 IP 地址。参见https://casbin.org/docs/en/function。使用内置函数我们能很容易对路由进行权限划分:

[matchers] m = r.sub == p.sub && keyMatch(r.obj p.obj) && r.act == p.act

p dajun user/dajun/* read p lizi user/lizi/* read

不同用户只能访问其对应路由下的 URL:

funcmain(){ e err:=casbin.NewEnforcer("./model.conf" "./policy.csv") iferr!=nil{ log.Fatalf("NewEnforecerfailed:%v\n" err) } check(e "dajun" "user/dajun/1" "read") check(e "lizi" "user/lizi/2" "read") check(e "dajun" "user/lizi/1" "read") }

输出:

dajun CAN read user/dajun/1 lizi CAN read user/lizi/2 dajun CANNOT read user/lizi/1

我们当然也可以定义自己的函数。先定义一个函数,返回 bool:

funcKeyMatch(key1 key2string)bool{ i:=strings.Index(key2 "*") ifi==-1{ returnkey1==key2 } iflen(key1)>i{ returnkey1[:i]==key2[:i] } returnkey1==key2[:i] }

这里实现了一个简单的正则匹配,只处理*。

然后将这个函数用interface{}类型包装一层:

funcKeyMatchFunc(args...interface{})(interface{} error){ name1:=args[0].(string) name2:=args[1].(string) return(bool)(KeyMatch(name1 name2)) nil }

然后添加到权限认证器中:

e.AddFunction("my_func" KeyMatchFunc)

这样我们就可以在匹配器中使用该函数实现正则匹配了:

[matchers] m = r.sub == p.sub && my_func(r.obj p.obj) && r.act == p.act

接下来我们在策略文件中为dajun赋予权限:

p dajun data/* read

dajun对匹配模式data/*的文件都有read权限。

验证一下:

check(e "dajun" "data/1" "read") check(e "dajun" "data/2" "read") check(e "dajun" "data/1" "write") check(e "dajun" "mydata" "read")

dajun对data/1没有write权限,mydata不符合data/*模式,也没有read权限:

dajun CAN read data/1 dajun CAN read data/2 dajun CANNOT write data/1 dajun CANNOT read mydata

总结

casbin功能强大,简单高效,且多语言通用。值得学习。

大家如果发现好玩、好用的 Go 语言库,欢迎到 Go 每日一库 GitHub 上提交 issue

参考

  1. casbin GitHub:https://github.com/casbin/casbin
  2. casbin 官网:https://casbin.org/
  3. 一种基于元模型的访问控制策略描述语言:http://www.jos.org.cn/html/2020/2/5624.htm
  4. Go 每日一库 GitHub:https://github.com/darjun/go-daily-lib

猜您喜欢: