casbin权限模型推演
无论什么项目只要涉及到多个用户的操作都会开始考虑权限控制, 权限管理是一个很常见部分,所以出现了单独处理这个部分的开源项目,即本文要介绍的casbin项目。
casbin支持很多的编程语言, 本文选择golang作为使用语言。 认证还是授权?
在大型项目中认证(Authentication)和授权(Authorization)一般是分开的,前者用于甄别用户是谁, 而后者用于判断用户有什么权限,授权不管认证, 认证也不管授权, 这点很重要, 但是很多时候为了简单会将两者放在一起,比如仅是判断用户是否认证,认证了就可以访问该资源,而本文主要讨论授权的问题, 所以不会关心认证的问题, 即没有验证用户名密码是否认证通过的业务逻辑。 准入
权限最开始总是准入,即只有两个选择,允许或者拒绝。
比如这样的场景,存在四个用户,zhangsan, lisi, wangwu, zhaoliu, 我们允许前三人可以访问我们的网站。
所以我们将策略描述如下. zhangsan lisi wangwu
在这个列表中的用户表示允许,反之不允许。 资源控制
随着项目的增长我们自然发现用户可以操作的项目多起来了, 所以需要在之前的策略里加上用户对应的资源。
比如这样的场景, 还是之前那四个用户,但是我们的网站变成了2个, 我们允许张三访问两者,后三人只能访问第二个网站。
所以我们将策略描述如下. zhangsan, web1 zhangsan, web2 lisi, web2 wangwu, web2 zhaoliu, web2
还是一样的逻辑,只是多了一个字段
我们可以将第一列称为user,第二列称为website。
与此同时,我们还需要对用户有更精细的控制,比如张三可以读写所有网站,但是其他三个人只读第二个网站,所以策略可以描述如下。 zhangsan, web1, read zhangsan, web1, write zhangsan, web2, read zhangsan, web2, write lisi, web2, read wangwu, web2, read zhaoliu, web2, read
我们将第三列称为action
至此我们基本上完成了权限控制,但是稍稍有点不完美, 这里的不完美是策略文件跟匹配模型太耦合, 比如我们还是上面的策略文件,但是网站变成了10个,并且我们希望只要在列表中的用户就能访问所有网站,又或者不管第二个字段,只根据第三个字段来判断用户的可读可写权限,最简单直接的办法自然是直接重写一遍策略文件并相应的修改代码,但是太无聊太枯燥了,所以我们需要将其 模型 提炼出来。
比如我们可以定义一个这样的模型 # 策略定义的意思 [policy_definition] p = user, website, action # 匹配逻辑 [matchers] m = user == p.user && website == website && action == action
这样我们就可以解决之前的问题了
比如只匹配第一个字段, 那我们可以定义如下 # 策略定义的意思 [policy_definition] p = user, website, action # 匹配逻辑 [matchers] m = user == p.user
又比如只匹配第一个和第三个字段 # 策略定义的意思 [policy_definition] p = user, website, action # 匹配逻辑 [matchers] m = user == p.user && action == p.action
至此我们可以在不改动策略文件的情况下仅仅改变比较小的内容就可以很快的完成匹配模型的转换,这样就会灵活很多,但是现在的模型还有些不太严谨, 我们通过 p = user, website, action 定义了策略文件的各个字段,却没有定义用户请求的各个字段, 比如要求用户请求应该填上哪些字段,所以我们需要再次改一下我们的匹配模型,修改如下: # 请求定义的意思 [request_definition] r = user, website, action # 策略定义的意思 [policy_definition] p = user, website, action # 匹配逻辑 [matchers] m = r.user == p.user && r.action == p.action
这样子我们的匹配模型看起来要严谨许多了,但是模型中请求定义(request_definition), 策略定义(policy_definition)在toml中的语法其实都是列表, 即我们可以定义多个策略和请求定义,比如: # 请求定义的意思 [request_definition] r = user, website, action r2 = user, action # 策略定义的意思 [policy_definition] p = user, website, action p2 = user, action # 匹配逻辑 [matchers] m = r.user == p.user && r.website == p.website && r.action == p.action m2 = r2.user == p2.user && r2.action == p2.action m3 = r.user == p.user && r.action == p.action
这样我们可以在一套模型中定义多个不同的组合,比如(r,p,m), (r2,p2,m2), (r,p,m3), 总的来说我们的匹配模型灵活性大大提高,但是我们的策略模型可能出现了不严谨的地方,即策略文件中的每一行是策略p, 还是策略p2? 我们无法判断,所以为了解决这个问题,我们需要在策略文件中多加一个字段.
策略文件定义如下: p, zhangsan, web1, read p, zhangsan, web1, write p, zhangsan, web2, read p, zhangsan, web2, write p, lisi, web2, read p, wangwu, web2, read p, zhaoliu, web2, read p2, sunqi, read
可以看到,我们增加了一个用户sunqi, 他不需要定义website,因为策略p2不需要website这个字段。
现在我们稍稍将名称再提炼一下,假设我们多了程序接口,也就是说使用者不是用户而是终端,我们可以称其为用户,但是稍稍有些别扭,我们可以将其统称为主体(subject),我们的项目也不可能总是网站,所以我们可以称其为对象(object), 而操作权限我们可以归纳为动作(action).
所以仅仅是为了让我们的模型的语言看起来更加的泛化,所以我们将其改成如下 # 请求定义的意思 [request_definition] r = subject, object, action r2 = subject, action # 策略定义的意思 [policy_definition] p = subject, object, action p2 = subject, action # 匹配逻辑 [matchers] m = r.subject == p.subject && r.object == p.object && r.action == p.action m2 = r2.subject == p2.subject && r2.action == p2.action m3 = r.subject == p.subject && r.action == p.action m3 = r.subject == p.subject && r.action == p.action
这个时候又来了新的需求,即再多加一个用户(zhouba),只需要这个用户不能访问web10(假设已经有10个网站。)即可,一种做法是为这个用户添加18条记录,即web1,web2,...,we9, 分别对应read和write, 作为一个程序员自然是讨厌这些枯燥无聊的工作的。
所以我们可以继续改进我们的匹配模型, 我们发现我们的匹配模型对于结果的判断过于单一,即只能允许,我们无法在已有的框架下扩展,这其实是因为我们没有处理匹配的结果,我们应该对匹配的结果进一步做处理,我们可以将匹配的结果称之为result, 而result有允许和拒绝(deny)两种, 在这个结果下,我们可以定义这样的语法,匹配到任意一个允许(allow)就放行,又或者没有任何一个拒绝(deny)就放行,而后者就是我们想要的解决方案,这样我们只要写一条拒绝访问web10的规则就可以达到目的。
所以模型定义如下: # 请求定义的意思 [request_definition] r = subject, object, action r2 = subject, action # 策略定义的意思 [policy_definition] p = subject, object, action p2 = subject, action p3 = subject, object, result # 策略结果的意思 [policy_result] e = some(where (p.result == allow)) e2 = !some(where (p.result == deny)) # 匹配逻辑 [matchers] m = r.subject == p.subject && r.object == p.object && r.action == p.action m2 = r2.subject == p2.subject && r2.action == p2.action m3 = r.subject == p.subject && r.action == p.action m3 = r.subject == p.subject && r.action == p.action m4 = r.subject == p.subject && r.object == p3.object
这里我们多定义了一个段落policy_result, 这个段落有两个执行逻辑,前者代表只要有一行的策略匹配结果是allow就放行,后者是没有一行的策略匹配结果是deny就放行。
为啥语法要定义成这样? 因为这是casbin的语法, 我只是拙劣的模仿,并且按照自己的理解来一步步推到casbin模型...语法只是一套要记住的规则而已,如果我们不需要自己解析的话,死记硬背即可,当然了,它的这个语法也不是难以理解的那种。
而新的策略规则如下: p, zhangsan, web1, read p, zhangsan, web1, write p, zhangsan, web2, read p, zhangsan, web2, write p, lisi, web2, read p, wangwu, web2, read p, zhaoliu, web2, read p2, sunqi, read p3, sunqi, web10, deny
至此整个模型基本完成了,可以适配大多数的访问控制(ACL)情况了,但是对于RBAC还是有些问题,但是这里就不继续演进了。后面通过代码来看看ACL, RBAC的是用。
当然了,你可能觉得模型的演进还是有很多问题,比如casbin使用的是简写sub,而这里使用的是subject全称,这里策略效果写的段落名是policy_result,而casbin写的是policy_effect, 不过这些不同之处在我看来只是小问题,只需替换即可。 代码示例
这一节直接使用golang来演示。 ACL
假设场景: 存在网站web1,web2, 张三可读写两者,李四只读web1。
模型定义如下: [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
策略定义如下: p, zhangsan, web1, read p, zhangsan, web1, read p, zhangsan, web2, read p, zhangsan, web2, write p, lisi, web1, read
代码如下: package acl1 import ( "log" "testing" "github.com/stretchr/testify/assert" "github.com/casbin/casbin/v2" ) func TestACL1(t *testing.T) { e, err := casbin.NewEnforcer("model.conf", "policy.csv") if err != nil { log.Fatal("创建策略引擎失败: ", err) } tests := [][]interface{}{ {"zhangsan", "web1", "read"}, {"zhangsan", "web1", "write"}, {"zhangsan", "web2", "read"}, {"zhangsan", "web2", "write"}, {"zhangsan", "webx", "write"}, {"lisi", "web1", "read"}, {"lisi", "web2", "read"}, {"lisi", "webx", "read"}, } expected := []bool{true, true, true, true, false, true, false, false} for i := 0; i < len(tests); i++ { ok, err := e.Enforce(tests[i]...) if err != nil { t.Fatalf("请求: %v 对应的期待是是: %t, 发生错误: %s", tests[i], expected[i], err) } assert.Equal(t, expected[i], ok) } }
在此基础上我们需要一个超级管理员,就称其为root
所以模型如下: [request_definition] r = sub, obj, act [policy_definition] p = sub, obj, act [policy_effect] e = some(where (p.eft == allow)) [matchers] # 唯一的不同是是加了|| p.sub == "root", 只要用户名是root就允许 m = r.sub == p.sub && r.obj == p.obj && r.act == p.act || p.sub == "root"
策略文件不变。
测试代码如下 package acl1 import ( "fmt" "log" "testing" "github.com/stretchr/testify/assert" "github.com/casbin/casbin/v2" ) func TestACL2(t *testing.T) { e, err := casbin.NewEnforcer("model.conf", "policy.csv") if err != nil { log.Fatal("创建策略引擎失败: ", err) } tests := [][]interface{}{ {"zhangsan", "web1", "read"}, {"zhangsan", "web1", "write"}, {"zhangsan", "web2", "read"}, {"zhangsan", "web2", "write"}, {"zhangsan", "webx", "write"}, {"lisi", "web1", "read"}, {"lisi", "web2", "read"}, {"lisi", "webx", "read"}, {"root", "web1", "read"}, {"root", "webx", "update"}, } expected := []bool{true, true, true, true, false, true, false, false, true, true} for i := 0; i < len(tests); i++ { ok, err := e.Enforce(tests[i]...) fmt.Println(tests[i], expected[i]) if err != nil { t.Fatalf("请求: %v 对应的期待是是: %t, 发生错误: %s", tests[i], expected[i], err) } assert.Equal(t, expected[i], ok) } }
测试结果也是通过的,可以看到root的测试用例中即使请求不存在的资源或者不存在的操作也是true, 因为模型中只判断用户是否为root。 RBAC
假设场景: 存在网站web1,web2, 可读角色(reader)可读写两个web,可读角色(writer)可写两个web,管理员角色(admin)可读写两者, 张三属于可读角色,李四属于可写角色,王五属于admin角色,赵六既属于可读角色也属于可写角色。
模型描述如下: [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
RBAC与ACL的不同之处在于多了一个role_definition, 多了一层抽象自然需要一个新的定义,这没什么奇怪的,就像request_definition, policy_definition. 不过它的语法又稍稍不同,首先它不用sub, obj之类的对象名称,仅用"_"作为所需参数的占位符, 两个下划线说明需要两个参数。
比较难以理解的是 g(r.sub, p.sub) , g是role_definition定义的一个角色操作符, 但是这个需要对照策略文件查看。
策略文件如下: # 策略定义 p, reader, web1, read p, reader, web2, read p, writer, web1, write p, writer, web2, write p, admin, web1, read p, admin, web1, write p, admin, web2, read p, admin, web2, write # 定义用户属于哪些角色 g, zhangsan, reader g, lisi, writer g, wangwu, admin g, zhaoliu, reader g, zhaoliu, writer
策略文件中分为两个部分,第一部分属于常见策略定义,不过这里定义的主体(sub)是后面定义的角色,即角色绑定到了具体的对象及操作,而g定义了用户属于哪些角色,比如 g, zhangsan, reader 代表zhangsan属于可读角色(reader),而可读角色(reader)可以读写web1, web2, 从来可以推导出zhangsan可读web1,web2。
在回过头看 g(r.sub, p.sub) 我们可以理解为g操作符将 r.sub 映射成了对应的角色,再将其角色与 p.sub 比较。因为请求中没有角色的数据,所以必然需要一个映射函数将其转换成对应的角色,casbin使用的角色定义的 g 。
测试代码如下: package acl1 import ( "fmt" "log" "testing" "github.com/stretchr/testify/assert" "github.com/casbin/casbin/v2" ) func TestRBAC(t *testing.T) { e, err := casbin.NewEnforcer("model.conf", "policy.csv") if err != nil { log.Fatal("创建策略引擎失败: ", err) } tests := [][]interface{}{ {"zhangsan", "web1", "read", true}, {"zhangsan", "web2", "read", true}, {"zhangsan", "web1", "write", false}, {"lisi", "web1", "write", true}, {"lisi", "web2", "write", true}, {"lisi", "web1", "read", false}, {"wangwu", "web1", "read", true}, {"wangwu", "web1", "read", true}, {"zhaoliu", "web1", "read", true}, {"zhaoliu", "web2", "write", true}, } for i := 0; i < len(tests); i++ { ok, err := e.Enforce(tests[i][:3]...) fmt.Println(tests[i], tests[i][3]) if err != nil { t.Fatalf("请求: %v 对应的期待是是: %t, 发生错误: %s", tests[i], tests[i][3], err) } assert.Equal(t, tests[i][3], ok) } }
测试自然是成功的。
值得注意的是: 虽然策略里面定义了角色的权限,但是也可以定义用户的权限,比如加一行 p, zhangsan, web1, write , 可也是可以的,但是初学起来觉得奇怪。熟悉之后可以任意的测试和组合。 上下文切换
在之前的模型推导过程中,模型总是定义了不止一个策略,不止一个匹配器,那么怎么在代码中体现呢?
模型定义如下: [request_definition] r = sub, obj, act r2 = sub, obj [policy_definition] p = sub, obj, act p2 = sub, obj [policy_effect] e = some(where (p.eft == allow)) e2 = some(where (p.eft == allow)) [matchers] m = r.sub == p.sub && r.obj == p.obj && r.act == p.act m2 = r2.sub == p2.sub && r2.obj == p2.obj
会发现每个对象都多了一份, 比如r2的定义说明只需要两个参数,而r需要三个参数,其他意思差不多。
策略定义如下: # 策略定义 p, zhangsan, web1, read p, zhangsan, web2, read p2, wangwu, web1 p2, wangwu, web2
分别为策略p, p2定义不同的策略,需要的参数不同
测试代码如下: package acl1 import ( "log" "testing" "github.com/stretchr/testify/assert" "github.com/casbin/casbin/v2" ) func TestRBAC(t *testing.T) { e, err := casbin.NewEnforcer("model.conf", "policy.csv") if err != nil { log.Fatal("创建策略引擎失败: ", err) } tests1 := [][]interface{}{ {"zhangsan", "web1", "read", true}, {"zhangsan", "web2", "read", true}, {"zhangsan", "web1", "write", false}, {"wangwu", "web1", "write", false}, {"wangwu", "web2", "write", false}, } ctx2 := casbin.NewEnforceContext("2") tests2 := [][]interface{}{ {ctx2, "wangwu", "web1", true}, {ctx2, "wangwu", "web2", true}, {ctx2, "wangwu", "web3", false}, } for i := 0; i < len(tests1); i++ { ok, err := e.Enforce(tests1[i][:3]...) // t.Log(tests1[i], tests1[i][3]) if err != nil { t.Fatalf("请求: %v 对应的期待是是: %t, 发生错误: %s", tests1[i], tests1[i][3], err) } assert.Equal(t, tests1[i][3], ok) } for i := 0; i < len(tests2); i++ { ok, err := e.Enforce(tests2[i][:3]...) // t.Log(tests2[i], tests2[i][3]) if err != nil { t.Fatalf("请求: %v 对应的期待是是: %t, 发生错误: %s", tests2[i], tests2[i][3], err) } assert.Equal(t, tests2[i][3], ok) } }
这与之前的不同在于第一个参数是context,这里为了简单没有单独的设置各个部分的值,比如这里的 casbin.NewEnforceContext("2") 说明使用(r2,p2,e2,m2), 但是e2跟e分明是一样的,所以可以单独设置context的EType为 "e" , 这里就不展开了… 一些额外的技巧
一些常使用的技巧 黑名单策略
本文全篇都是白名单策略,即允许才放行,但是有时候很名单更有效,比如网站的反爬策略,大多数链接都是允许的,只有一部分是不允许的, 所以用白名单去放行所有资源显然有点不现实及不高效,所以我们可以将策略结果进行如下设置 [policy_effect] e = !some(where (p.eft == deny))
该语法声明的是,不存在任何决策结果为 deny 的匹配规则,则最终决策结果为 allow
但是什么时候 p.eft == deny 呢? 其实策略定义中可配置eft这个属性,定义如下 [policy_definition] p = sub, obj, act, eft
然后对应的策略定义如下: p, zhangsan, web1, read, allow p, zhangsan, web2, read, deny数据源适配
一般来说模型文件是放在本地,并且很少更改,而策略文件可以放在很多地方,比如数据库,代码如下。 import ( "log" "github.com/casbin/casbin/v2" "github.com/casbin/casbin/v2/model" xormadapter "github.com/casbin/xorm-adapter/v2" _ "github.com/go-sql-driver/mysql" ) // 使用MySQL数据库初始化一个Xorm适配器 a, err := xormadapter.NewAdapter("mysql", "mysql_username:mysql_password@tcp(127.0.0.1:3306)/casbin") if err != nil { log.Fatalf("error: adapter: %s", err) } m, err := model.NewModelFromString(` [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 `) if err != nil { log.Fatalf("error: model: %s", err) } e, err := casbin.NewEnforcer(m, a) if err != nil { log.Fatalf("error: enforcer: %s", err) }
代码摘自: https://casbin.org/zh/docs/get-started casbin编辑器
在线地址: https://casbin.org/zh/editor
在线编辑器虽然可以很方便的验证想法,但是使用稍稍有些限制,只不过对于大多数人不是问题,因为不需要上下文切换。 总结
自己写一个ACL或者RBAC倒是不太复杂,但是枯燥无味就像写CRUD一样并且不够灵活,而Casbin是比较强大的,它支持超级多的模型,ACL, RBAC, ABAC等多种策略模型。
女性过了50岁,体重多少斤比较合适?对照一下,可能你并不胖爱美之心人皆有之,不管到了什么年纪,发现每一个人都希望自己的更美更年轻,不管男女都想要拥有高颜值及苗条性感的身材。比如现代人尤其是女性,非常在乎自己的容貌,很在乎自己的身材,在生活
对自己现状的无能为力,清醒一点本人来自一个普通的农村家庭,曾经考试考过倒数,家境不好。成绩也不够好。既没人关心,更没人在乎。也没有很好的朋友,或者那些所谓的朋友,都靠不住。长相勉勉勉强强过得去。家境贫寒,成绩倒
儿童软色情成了家长的致富经,这些大尺度擦边球表情包你用过吗?Withthecontinuousdevelopmentofthetimesandeconomy,moreandmorepeoplehavejoinedtheInternetind
穿过乏力沼泽2022手机出海新变局作者郭照川编辑麻吉国产智能手机出海,表面上风平浪静,暗地里波涛汹涌。市场上有不少声音称,中国智能手机市场已进入增长谷底。根据中国通信院的数据,国内手机市场上半年累计出货量为1。36
塔元庄村在奋进新征程中谱写乡村振兴新篇章塔元庄村在奋进新征程中谱写乡村振兴新篇章李子佳张廷伟正定县城西,滹沱河北岸,盛夏的塔元庄村草木葱茏鲜花盛开,一派生机盎然欣欣向荣的多彩景象。知之深爱之切。上世纪80年代,习近平同志
整装待发兰石重装属机械制造行业,主要为石化行业加工制造重型装备,现延伸至新能源装备(核能太阳能多晶硅氢能)工业智能装备及节能环保装备设计制造及工程总承包。列入工业母机概念也不为过,商业模式
乳腺自查每个女生都应该上的必修课大家好,我是贯剑,一些女性患者在日常触摸乳腺时,发现乳腺有肿块,到医院检查,医生告知这是乳腺结节,患者立刻担心这是不是意味着离乳腺癌不远了?乳腺结节属中医乳癖证,中医认为外邪侵入七
星辰变猿猴一族出现过神王天尊,为什么还被龙族压制呢?前言谁才是仙魔妖界第一大族群呢?有的人说是龙族,因为龙皇是妖界第一高手,强大如屋蓝这样高手面对龙皇都甘拜下风。有人说是飞禽一族,因为飞禽一族有鹏魔皇倪皇这样的一流高手,其巅峰战力要
以为买了个壁虎,却没想到越长越大!这是养着养着就进化了吗?相亲对象看到我开的车脸色就变了这究竟是为什么关系要是不铁都不敢拿这个来看望病人这个看着怕是有点疼了啊工地高价请来的大厨说是祖传的手艺应该相信他吗自己家的车当街砸有错吗小伙子你可能还
一波又一波窜台,究竟是为了什么?自打佩婆子2号晚上窜台以来,这些天一波接一波的总是爆出不少国外反动势力窜台的消息,这些势力短期内造成什么严重后果没有,我思索了一下,还真没有,不过这种你方唱罢我登场的过来,真跟往自
台湾海峡是单行道,不适应美军航母通行裴洛西8月2日抵达台湾,里根号那时距离台湾最近。3日下午5点,裴洛西一行离开台湾后,里根号迅速远离台湾。8月4日中午12点,解放军开始演训,里根号则离得更远,8月5日几乎已经到日本