Authorization in Golang ProjectUseing Casbin

一、Casbin介绍

权限管理在几乎每个系统中都是必备的模块。如果项目开发每次都要实现一次权限管理,无疑会浪费开发时间,增加开发成本。

因此,casbin库出现了。casbin是一个强大、高效的访问控制库。支持常用的多种访问控制模型,如ACL/RBAC/ABAC等。可以实现灵活的访问权限控制。同时,casbin支持多种编程语言,Go/Java/Node/PHP/Python/.NET/Rust。我们只需要一次学习,多处运用。

Casbin 可以

  1. 支持自定义请求的格式,默认的请求格式为{subject, object, action}
  2. 具有访问控制模型model和策略policy两个核心概念。
  3. 支持RBAC中的多层角色继承,不止主体可以有角色,资源也可以具有角色。
  4. 支持内置的超级用户 例如:root 或 administrator。超级用户可以执行任何操作而无需显式的权限声明。
  5. 支持多种内置的操作符,如 keyMatch,方便对路径式的资源进行管理,如 /foo/bar 可以映射到 /foo*

Casbin 不能

  1. 身份认证 authentication(即验证用户的用户名和密码),Casbin 只负责访问控制。应该有其他专门的组件负责身份认证,然后由 Casbin 进行访问控制,二者是相互配合的关系。
  2. 管理用户列表或角色列表。 Casbin 认为由项目自身来管理用户、角色列表更为合适, 用户通常有他们的密码,但是 Casbin 的设计思想并不是把它作为一个存储密码的容器。 而是存储RBAC方案中用户和角色之间的映射关系。

二、快速使用

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

$ mkdir learning-node && cd learning-node
$ go mod init learning-node 

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

go get -u github.com/casbin/casbin/v2

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

  • policy,是策略或者说是规则的定义。它定义了具体的规则,例如以下示例:
p = {sub, obj, act, eft}

策略规则描述:subject(sub访问实体),object(obj访问的资源)和action(act访问方法)eft(策略结果一般为空默认指定allow),通常策略一般存储到关系数据表中,因为会有很多。

  • request,是对访问请求的抽象,它与e.Enforce()函数的参数是一一对应的。
  • matcher,匹配器会将请求与定义的每个policy一一匹配,生成多个匹配结果。
  • effect,根据对请求运用匹配器得出的所有结果进行汇总,来决定该请求是允许allow)还是拒绝deny)访问。

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

ACL模型(Access Control List,访问控制列表)

首先在learning-node 文件夹下定义一个模型model.conf文件:

[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))表示只要有一条策略允许即可,其它策略请见:(点我跳转)。

然后我们在learning-node 文件夹下定义一个策略policy.csv文件(即谁能对什么资源进行什么操作):

p, zhangsan, data1, read
p, lisi, data2, write

上面policy.csv文件的两行内容表示zhangsan对数据data1read权限,lisi对数据data2write权限;

然后在learning-node 文件夹下新建 main.go 文件

package main

import (
  "fmt"
  "log"

  "github.com/casbin/casbin/v2"
)
// ACL 权限访问检查
func check(e *casbin.Enforcer, sub, obj, act string) {
  ok, _ := e.Enforce(sub, obj, act)
  if ok {
    fmt.Printf("%s CAN %s %s\n", sub, act, obj)
  } else {
    fmt.Printf("%s CANNOT %s %s\n", sub, act, obj)
  }
}

func main() {
  e, err := casbin.NewEnforcer("path/to/model.conf", "path/to/policy.csv")
  if err != nil {
    log.Fatalf("NewEnforecer failed:%v\n", err)
  }

  check(e, "zhangsan", "data1", "read")
  check(e, "lisi", "data2", "write")
  check(e, "zhangsan", "data1", "write")
  check(e, "zhangsan", "data2", "read")
}

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

$ go run main.go
// echo result
zhangsan CAN read data1
lisi CAN write data2
zhangsan CANNOT write data1
zhangsan CANNOT read data2

请求必须完全匹配policy.csv某条策略才能通过。

第 1 个因为("zhangsan", "data1", "read")匹配p, zhangsan, data1, read

第 2 个因为("lisi", "data2", "write")匹配p, lisi, data2, write

第 3 个因为"zhangsan"没有对data1write权限。

第 4 个因为zhangsandata2没有read权限,所以检查都不能通过。

以上所有输出结果符合预期。

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

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

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

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

验证:

func main() {
  e, err := casbin.NewEnforcer("path/to/model.conf", "path/to/policy.csv")
  if err != nil {
    log.Fatalf("NewEnforecer failed:%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模型(Role Based Access Control)

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

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

[role_definition]
g = _, _

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

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

p, admin, data, read
p, admin, data, write
p, developer, data, read
g, zhangsan, admin
g, lisi, developer

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

package main

import (
  "fmt"
  "log"

  "github.com/casbin/casbin/v2"
)
// RBAC 权限访问检查
func check(e *casbin.Enforcer, sub, obj, act string) {
  ok, _ := e.Enforce(sub, obj, act)
  if ok {
    fmt.Printf("%s CAN %s %s\n", sub, act, obj)
  } else {
    fmt.Printf("%s CANNOT %s %s\n", sub, act, obj)
  }
}

func main() {
  e, err := casbin.NewEnforcer("path/to/model.conf", "path/to/policy.csv")
  if err != nil {
    log.Fatalf("NewEnforecer failed:%v\n", err)
  }

  check(e, "zhangsan", "data", "read")
  check(e, "zhangsan", "data", "write")
  check(e, "lisi", "data", "read")
  check(e, "lisi", "data", "write")
}

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

zhangsan CAN read data
zhangsan CAN write data
lisi CAN read data
lisi CANNOT write data

多个RBAC 模型(Multiple Rbac)

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

[role_definition]
g=_,_
g2=_,_

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

上面的模型文件定义了两个RBAC系统gg2,我们在匹配器中使用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, zhangsan, admin
g, lisi, developer
g2, prod.data, prod
g2, dev.data, dev

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

check(e, "zhangsan", "prod.data", "read")
check(e, "zhangsan", "prod.data", "write")
check(e, "lisi", "dev.data", "read")
check(e, "lisi", "dev.data", "write")
check(e, "lisi", "prod.data", "write")

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

zhangsan CAN read prod.data
zhangsan CAN write prod.data
lisi CAN read dev.data
lisi CAN write dev.data
lisi CANNOT write prod.data

多层角色(Multi Layered Roles)

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

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

p, senior, data, write
p, developer, data, read
g, zhangsan, senior
g, senior, developer
g, lisi, developer

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

check(e, "zhangsan", "data", "read")
check(e, "zhangsan", "data", "write")
check(e, "lisi", "data", "read")
check(e, "lisi", "data", "write")
结果不出意料:
zhangsan CAN read data
zhangsan CAN write data
lisi CAN read data
lisi CANNOT write data

RBAC domain 模型

casbin中,角色可以是全局的,也可以是特定domain(领域)或tenant(租户),可以简单理解为。例如zhangsan在组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, zhangsan, admin, tenant1
g, zhangsan, developer, tenant2

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

func check(e *casbin.Enforcer, sub, domain, obj, act string) {
  ok, _ := e.Enforce(sub, domain, obj, act)
  if ok {
    fmt.Printf("%s CAN %s %s in %s\n", sub, act, obj, domain)
  } else {
    fmt.Printf("%s CANNOT %s %s in %s\n", sub, act, obj, domain)
  }
}

func main() {
  e, err := casbin.NewEnforcer("path/to/model.conf", "path/to/policy.csv")
  if err != nil {
    log.Fatalf("NewEnforecer failed:%v\n", err)
  }

  check(e, "zhangsan", "tenant1", "data1", "read")
  check(e, "zhangsan", "tenant2", "data2", "read")
}

结果不出意料:

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

ABAC模型(Attribute Base Access List)

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

定义一个模型model.conf文件:

[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))

该规则不需要策略文件:

type Object struct {
  Name  string
  Owner string
}

type Subject struct {
  Name string
  Hour int
}

func check(e *casbin.Enforcer, sub Subject, obj Object, act string) {
  ok, _ := e.Enforce(sub, obj, act)
  if ok {
    fmt.Printf("%s CAN %s %s at %d:00\n", sub.Name, act, obj.Name, sub.Hour)
  } else {
    fmt.Printf("%s CANNOT %s %s at %d:00\n", sub.Name, act, obj.Name, sub.Hour)
  }
}

func main() {
  e, err := casbin.NewEnforcer("path/to/model.conf")
  if err != nil {
    log.Fatalf("NewEnforecer failed:%v\n", err)
  }

  o := Object{"data", "zhangsan"}
  s1 := Subject{"zhangsan", 10}
  check(e, s1, o, "read")

  s2 := Subject{"lisi", 10}
  check(e, s2, o, "read")

  s3 := Subject{"zhangsan", 20}
  check(e, s3, o, "read")

  s4 := Subject{"lisi", 20}
  check(e, s4, o, "read")
}

结果不出意料:

zhangsan CAN read data at 10:00
lisi CAN read data at 10:00
zhangsan CAN read data at 20:00
lisi CANNOT read data at 20:00

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

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

模型存储

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

func main() {
  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)
  if err != nil {
    log.Fatalf("NewEnforecer failed:%v\n", err)
  }

  check(e, "zhangsan", "data1", "read")
  check(e, "lisi", "data2", "write")
  check(e, "zhangsan", "data1", "write")
  check(e, "zhangsan", "data2", "read")
}

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

func main() {
  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, "zhangsan", "data1", "read")
  check(e, "lisi", "data2", "write")
  check(e, "zhangsan", "data1", "write")
  check(e, "zhangsan", "data2", "read")
}

但是这两种方式并不推荐(不能持久化管理)。

策略存储

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

docker compose 快速部署 mysql (点我跳转

CREATE DATABASE IF NOT EXISTS casbin;

USE casbin;

create table casbin_rule
(
    id    bigint unsigned auto_increment primary key,
    ptype varchar(100) null,
    v0    varchar(100) null,
    v1    varchar(100) null,
    v2    varchar(100) null,
    v3    varchar(100) null,
    v4    varchar(100) null,
    v5    varchar(100) null,
    v6    varchar(25)  null,
    v7    varchar(25)  null,
    constraint idx_casbin_rule unique (ptype, v0, v1, v2, v3, v4, v5, v6, v7)
);


INSERT INTO casbin_rule (ptype, v0, v1, v2)
VALUES ('p', 'zhangsan', 'data1', 'read'),
       ('p', 'lisi', 'data2', 'write');

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

package main

import (
  "fmt"

  "github.com/casbin/casbin/v2"
  gormadapter "github.com/casbin/gorm-adapter/v3"
  _ "github.com/go-sql-driver/mysql"
  "gorm.io/gorm/logger"
  "log"
)

func check(e *casbin.Enforcer, sub, obj, act string) {
  ok, _ := e.Enforce(sub, obj, act)
  if ok {
    fmt.Printf("%s CAN %s %s\n", sub, act, obj)
  } else {
    fmt.Printf("%s CANNOT %s %s\n", sub, act, obj)
  }
}

func main() {
  // 通过 Xorm实现,使用适配器初始化一个  MySQL (true:是会自动进行表的管理创建/更新)
  a, _ := gormadapter.NewAdapter("mysql", "root:123456789@tcp(127.0.0.1:13306)/casbin", true)
  // echo sql log
  a.AddLogger(logger.Default.LogMode(logger.Info))
  e, _ := casbin.NewEnforcer("path/to/model.conf", a)

  check(e, "zhangsan", "data1", "read")
  check(e, "lisi", "data2", "write")
  check(e, "zhangsan", "data1", "write")
  check(e, "zhangsan", "data2", "read")
}

结果不出意料:

zhangsan CAN read data1
lisi CAN write data2
zhangsan CANNOT write data1
zhangsan CANNOT read data2

使用函数

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

定义模型model.conf文件:

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

定义策略policy.csv文件(即谁能对什么资源进行什么操作):

p, zhangsan, user/zhangsan/*, read
p, lisi, user/lisi/*, read

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

func main() {
  e, err := casbin.NewEnforcer("path/to/model.conf", "path/to/policy.csv")
  if err != nil {
    log.Fatalf("NewEnforecer failed:%v\n", err)
  }

  check(e, "zhangsan", "user/zhangsan/1", "read")
  check(e, "lisi", "user/lisi/2", "read")
  check(e, "zhangsan", "user/lisi/1", "read")
}

输出:

zhangsan CAN read user/zhangsan/1
lisi CAN read user/lisi/2
zhangsan CANNOT read user/lisi/1

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

func KeyMatch(key1, key2 string) bool {
  i := strings.Index(key2, "*")
  if i == -1 {
    return key1 == key2
  }

  if len(key1) > i {
    return key1[:i] == key2[:i]
  }

  return key1 == key2[:i]
}

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

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

func KeyMatchFunc(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

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


p, zhangsan, data/*, read

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

验证:

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

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

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

三、总结

casbin 极大的减少了项目对权限验证的业务代码量构建,只要一次学习,即可多处使用,它的多适用性值得学习和应用到实际项目中。

四、笔记地址

gitlab地址:http://gitlab.lilogs.com/liang/learning-note/tree/master/casbin

五、参考

  1. Go 每日一库之 casbin:https://darjun.github.io/2020/06/12/godailylib/casbin/
  2. Casbin 官方文档:https://casbin.org/docs/zh-CN/overview
  3. Casbin GitHub:https://github.com/casbin
  4. 一种基于元模型的访问控制策略描述语言:http://www.jos.org.cn/html/2020/2/5624.htm

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇