目录

iris + casbin 从陌生到学会使用的过程

iris + casbin 从陌生到学会使用的过程

目录

前言

前面写了一篇 【iris-go 框架构建登陆 API 项目开发过程】 的文章,因为篇幅的关系文章并没有介绍如何实现权限控制。接下来我将会介绍如何使用 casbin 在 iris 的项目中实现权限控制,项目采用 grom 做数据处理。

本文纯属个人简陋观点和经验,如果有错误的地方,欢迎大家指出。

这里继续讲一些废话,原本 IrisAdminApi 项目采用的最原始的 RBAC 的权限控制实现,分别创建 roles , permissions 表,然后定义关联关系,写一些关联创建逻辑。在实现的过程发现不管是使用 gorm ,xorm,beego的orm,定义关联关系都挺麻烦的,特别是多对对关系,还有一些多态关系等等。可能 Laravel 使用习惯性了,发现框架功能太齐全,对自身的提升真的会有阻碍。

这时候我就想有没有别人实现好的权限控制轮子,结果一顿搜索,让我找到了 casbin。这就是我和 casbin 的初识了。

刚见到 casbin 其实有些不知道如何下手,毕竟没有见过类似的项目。相信很多新手也会有同样的感受。不过没关系,一回生两回熟,像我们这样经过九年义务教育的社会主义好青年,最不怕的就是学习新事物。

大致介绍下我学习 casbin 的思路:

  • 首先:看文档,就像买了新东西看说明书一样,学习任何新事物必不可少的一步。不需要完全背下来,大致有个印象就好。不懂的地方可以多看几遍,不要怕麻烦。
  • 接下来:在文档中找到我们需要的功能,我们需要的是一个与 gorm 工作的 RBAC 权限控制,正好我发现作者有写 gorm 相关的适配器,其实作者为不同语言写非常多的适配器,这样可以省去我们的很多时间。
  • 打开 gorm-adapter 项目,按照说明执行安装适配器:
1
go get github.com/casbin/gorm-adapter
  • 查看实例代码,很多项目多有实例代码,其实这些代码的价值比文档的价值更高,能让我们更好的理解项目。特别是注释,需要仔细看完。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
	"github.com/casbin/casbin/v2"
	gormadapter "github.com/casbin/gorm-adapter/v2"
	_ "github.com/go-sql-driver/mysql"
)

func main() {
	// 初始化一个 Gorm 适配器并且在一个 Casbin enforcer 中使用它:
	// 这个适配器会使用一个名为 "casbin" 的 MySQL 数据库。
	// 如果数据库不存在,适配器会自动创建它。 
	// 你同样也可以像这样 gormadapter.NewAdapterByDB(gormInstance) 使用一个已经存在的 gorm 实例。
	a := gormadapter.NewAdapter("mysql", "mysql_username:mysql_password@tcp(127.0.0.1:3306)/") //你的驱动和数据源
	e, _ := casbin.NewEnforcer("examples/rbac_model.conf", a)
	
	// 或者你可以像这样使用一个其他的数据库 "abc" :
	// 适配器会使用名为 "casbin_rule" 的数据表。
	// 如果数据表不存在,适配器会自动创建它。
	// a := gormadapter.NewAdapter("mysql", "mysql_username:mysql_password@tcp(127.0.0.1:3306)/abc", true)

	// 从数据库加载策略规则
	e.LoadPolicy()
	
	// 检查权限
	e.Enforce("alice", "data1", "read")
	
	// 更新策略
	// e.AddPolicy(...)
	// e.RemovePolicy(...)
	
	// 保存策略到数据库
	e.SavePolicy()
}
  • 看完这个实例的代码和注释,相信你已经了解 casbin 怎么使用了。如果还不了解,那就多看几遍。

  • 在看文档的过程中发现作者已经写好了 RBAC 相关的 API 。所以我们并不需要去重复的这些工作。你没有看到?这说明了仔细看文档的重要的性。

  • 现在我们接着前面的项目,将 casbin 功能添加到项目中。编辑 models/base.go 文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
var Db *gorm.DB 
var err error  
var dirverName string  
var conn string
var Enforcer *casbin.Enforcer
func Register() {
    dirverName = "mysql"
    if isTestEnv() { //如果是测试使用测试数据库
        conn = "root:wemT5ZNuo074i4FNsTwl4KhfVSvOlBcF@(127.0.0.1:3306)/tiris"
    } else {
        conn = "root:wemT5ZNuo074i4FNsTwl4KhfVSvOlBcF@(127.0.0.1:3306)/iris"
    }
    //初始化数据库
    Db, err = gorm.Open(dirverName, conn+"?charset=utf8&parseTime=True&loc=Local")
    if err != nil {
        color.Red(fmt.Sprintf("gorm open 错误: %v", err))
    }
	
	a := gormadapter.NewAdapter("mysql",conn) 
	Enforcer, _ = casbin.NewEnforcer("examples/rbac_model.conf", a)
	Enforcer.LoadPolicy()
}
  • 这里我们不需要生成新的 casbin 数据库,直接使用在 iris 数据库生成 casbin_rule 数据表。同时将 rbac_model.conf 文件复制到自己的项目的对应路径中。这样 casbin 就已经集成好了,重启项目就会生成对应的 casbin_rule 数据表了。
  • 为什么没有使用 e.AddPolicy(…),e.SavePolicy(),e.Enforce(“alice”, “data1”, “read”) 这些方法? 如果你还有这样的疑问,说明你没有看实例的注释,要么就是没有仔细阅读。请多看几遍。
  • 虽然我们已经集成了 casbin ,但是我们还没有实现权限控制的目的。如果实现?我们需要关联角色,权限等数据到用户,并且在用户访问相应资源的时候检查用户是否拥有权限。
  • 到这里有经验的朋友就会发现,我们需要一个中间件去实现用户权限的检测。那么我们移驾到 iris 的中间件文档,我们仔细的看以遍文档,你会发现已经有了 casbin 的中间件实现。并不需要我们自己编写。
  • 接着来到 middleware/tree/master/casbin

按照说明文档,先安装 casbin 还有 casbin-middleware,分别执行一下命令:

1
2
3
 go get github.com/casbin/casbin
 
 go get github.com/iris-contrib/middleware/casbin
  • 然后按照实例中的代码将中间件添加到路由:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package routers
import (
"IrisAdmin/controllers"
"IrisAdmin/middleware"
"IrisAdmin/models"
"github.com/kataras/iris/v12"
"github.com/casbin/casbin/v2"
cm "github.com/iris-contrib/middleware/casbin"
)
func Register(app *iris.Application) {
// 路由集使用跨域中间件 CrsAuth()
// 允许 Options 方法 AllowMethods(iris.MethodOptions)
main := app.Party("/", middleware.CrsAuth()).AllowMethods(iris.MethodOptions)
{
 v1 := main.Party("/v1")
 {
     v1.Post("/admin/login", controllers.UserLogin)
     v1.PartyFunc("/admin", func(admin iris.Party) {
		casbinMiddleware := cm.New(models.Enforcer)
        admin.Use(middleware.JwtHandler().Serve, middleware.AuthTokencasbinMiddleware.ServeHTTP) //登录验证
         admin.Get("/logout",  controllers.UserLogout).Name = "退出"
     })
 }
}
}
  • 这里中间件有一个新的 casbinmodel.conf文件,需要把里面的内容替换到前面提到的 rbac_model.conf 文件中。
  • 现在登陆项目访问任何接口都会出现 403 拒绝访问的状态返回,这说明中间件生效了。接下来我们需要使用 casbin 提供的 Api 将项目的用户,角色和权限相互关联起来。
  • 打开 rbac-api 页面,我们看到有一个 AddRoleForUser() 给用户添加角色的方法。我们将它添加到用户的创建和更新方法中,实现用户和角色的关联。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func CreateUser() (user *User) {
  salt, _ := bcrypt.Salt(10)
  hash, _ := bcrypt.Hash("password", salt)
  user = &User{
      Username: "username",
      Password: hash,
      Name:     "name",
  }
  if err := Db.Create(user).Error; err != nil {
      color.Red(fmt.Sprintf("CreateUserErr:%s \n ", err))
  }
	
	userId := strconv.FormatUint(uint64(user.ID), 10)
	if _, err := database.GetEnforcer().AddRoleForUser(userId, "1"); err != nil {
		color.Red(fmt.Sprintf("CreateUserErr:%s \n ", err))
	}
	
  return
}
  • 这里需要注意的是 AddRoleForUser() 函数接受的参数都是字符串,所以用户和角色的 Id 都需要转换一下格式。如果是多角色传入多个角色的 Id 循环添加即可。
  • 接下来我们要关联角色和权限, 在 rbac-api 页面,找了半天发现没有添加权限的方法。仔细看过文档多次之后,确定真的没有添加权限到角色的方法。那么,我们接下来该怎么办呢?我的解决方法是查看 casbin 的源码。因为我十分确定,添加权限的方法肯定是存在的,只是rbac-api 页面上没有而已。
  • 果然,我在文档找到了相关方法:
1
2
3
func (e *Enforcer) AddPolicy(params ...interface{}) (bool, error) {
	return e.AddNamedPolicy("p", params...)
}

是不是很眼熟。没错,就是在前面适配器实例中出现过的方法。它的说明是添加策略,当时没想到策略就是权限,真的是一叶障目,不见南山。众里寻他千百度…..

  • 既然找到方法,那么将角色关联权限(策略)的逻辑添加到角色的创建和更新当中。
1
2
3
4
5
6
7
8
roleId := strconv.FormatUint(uint64(role.ID), 10)
var perms []Permission
models.DB.Where("id in (?)", permIds).Find(&perms)
for _, perm := range perms {
	if _, err := models.Enforcer.AddPolicy(roleId, perm.Name, perm.Act); err != nil {
		color.Red(fmt.Sprintf("AddPolicy:%s \n", err))
	}
}

同样 AddPolicy() 函数参数都是字符串,角色 Id 也需要转换一下。

  • 所有数据都关联好了之后,我们创建好相关的角色和权限数据。用户就可以正常访问登陆了。详细代码请查看项目 IrisAdminApi

后记

这里记录一下如何使用 iris 自动生成权限(策略)的思路。

  • 首先,肯定要从路由入手,给路由都加上名称,方便区分权限。
1
2
3
4
5
6
7
8
app.PartyFunc("/users", func(users iris.Party) {
	users.Get("/", controllers.GetAllUsers).Name = "用户列表"
	users.Get("/{id:uint}", controllers.GetUser).Name = "用户详情"
	users.Post("/", controllers.CreateUser).Name = "创建用户"
	users.Put("/{id:uint}", controllers.UpdateUser).Name = "编辑用户"
	users.Delete("/{id:uint}", controllers.DeleteUser).Name = "删除用户"
	users.Get("/profile", controllers.GetProfile).Name = "个人信息"
})

这样就可以在获取路由的时候获取到对应的名称了。

  • 如何获取 iris 应用的路由信息?找了文档发现没有相关的方法。这时候我们只能取查看源码了。 在源码中我找到了如下源码:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// GetRoutes 返回路由的信息,
// 这些信息有些可以被修改,有些不可以。
//
// 需要刷新路由器到方法或路径或处理程序更改才能进行。
func (api *APIBuilder) GetRoutes() []*Route {
	return api.routes.getAll()
}

// GetRoute 基于路由名称返回已注册路由, 否则返回nil。
// 一个提示: "路由名称" 区分大小写。
func (api *APIBuilder) GetRoute(routeName string) *Route {
	return api.routes.get(routeName)
}

// GetRouteByPath 基于模版路径 (`Route.Tmpl().Src`) 返回已注册路由。
func (api *APIBuilder) GetRouteByPath(tmplPath string) *Route {
	return api.routes.getByPath(tmplPath)
}

// GetRoutesReadOnly 返回只读授权的已注册路由,
// 你不能也不应该在请求状态下改变路由的任何属性,
// 如果想要改变可以使用 `GetRoutes()`函数替代。
//
// 它返回基于接口的切片而不是实际切片,
// 以便在上下文(请求状态)和构建的应用程序之间安全提取。
//
// 同时请看 `GetRouteReadOnly` 。
func (api *APIBuilder) GetRoutesReadOnly() []context.RouteReadOnly {
	routes := api.GetRoutes()
	readOnlyRoutes := make([]context.RouteReadOnly, len(routes))
	for i, r := range routes {
		readOnlyRoutes[i] = routeReadOnlyWrapper{r}
	}
	return readOnlyRoutes
}

从以上源码中可以看出, GetRouteReadOnlyGetRoutes() 都满足我们的使用需求。我们使用 GetRoutesReadOnly()实现。

1
2
3
4
5
app = NewApp() // 初始化app
routes := app.GetRoutesReadOnly()

// 保存数据到数据库,详细代码查看 https://github.com/snowlyg/IrisAdminApi 
... 
  • 这里会有个小问题,就是数据没有问题,但是访问类似编辑,更新等后面带有 id 会变化的接口,会出现 403 返回。可是明明已经都有权限数据了,而且其他接口也能正常访问,为什么会这样? 检查发现原因是:生成的(权限)策略数据是这样的 /v1/admin/users/{id:uint}

image.png

实际上 casbin 匹配的权限应该是这样的 /dataset1/*,具体内容查看 casbinpolicy.csv

  • 这时候有两种解决思路:
    • 修改自动生成权限的逻辑,将 {id:uint},替换成 *
    • 修改 casbin 的匹配逻辑。 因为第一种比较简单,这里主要讨论第二种方式,既然要修改 casbin 的匹配逻辑。首先要了解它的原理,很不巧和很多刚接触 casbin 的朋友一样,我对这些也不是很了解。这时候我门只能通过查看文档,以及源码来更深入的了解它。
  • 点开文档 matchers 我查找到 matachers 部分适用于定于匹配规则的,但是这里没有更深层的解释要如何定义。
  • 继续查看文档 function,在这里我们能够到不同的匹配模式,甚至我们可以自定义匹配函数。而且我们发现其中有两种匹配方式可能会满足我们,keyMatch3keyMatch4都能匹配 {} 类似的表达式 。

image.png

那么我们来尝试修改 examples/rbac_model.conf 这个文件的内容,将 keyMatch改为keyMatch3,重启项目之后访问编辑相关接口,发现问题已经解决。(发现使用 keyMatch4,同样有效)

1
2
[matchers]
m = g(r.sub, p.sub) && keyMatch3(r.obj, p.obj) && (r.act == p.act || p.act == "*")
  • 还有另外一种方式:用来修改默认的匹配。
    • 首先修改 examples/rbac_model.conf 这个文件:去除中间的 && keyMatch3(r.obj, p.obj) 让 casbin 使用默认的匹配规则(默认为 keyMatch)。
1
2
[matchers]
m = g(r.sub, p.sub)  && (r.act == p.act || p.act == "*")
  • 然后在 models/base.go 文件增加代码:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import (
	defaultrolemanager "github.com/casbin/casbin/v2/rbac/default-role-manager"
	"github.com/casbin/casbin/v2/util"
)
......
Enforcer, _ = casbin.NewEnforcer("examples/rbac_model.conf", a)
// 修改默认匹配为 KeyMatch3
rm := defaultrolemanager.NewRoleManager(10).(*defaultrolemanager.RoleManager)
rm.AddMatchingFunc("KeyMatch3", util.KeyMatch3)
Enforcer.LoadPolicy()
......

此方法和上面直接修改 examples/rbac_model.conf 文件的方式更复杂一些,这里只是介绍一下提供另一种思路。

写完了,感觉有点乱。主要还是讲述我解决需求和问题的一个思路。希望对你有帮助。 还是那句话:能力有限,不喜勿喷。