前言
前面写了一篇 【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()
}
rbac_model.conf
按照说明文档,先安装 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.AuthToken,casbinMiddleware.ServeHTTP) //登录验证
admin.Get("/logout", controllers.UserLogout).Name = "退出"
})
}
}
}
casbinmodel.confrbac_model.conf403AddRoleForUser()
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()
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()
后记
这里记录一下如何使用 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
...
/v1/admin/users/{id:uint}
/dataset1/*
{id:uint}*keyMatch3keyMatch4{}
examples/rbac_model.confkeyMatchkeyMatch3keyMatch4
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)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
写完了,感觉有点乱。主要还是讲述我解决需求和问题的一个思路。希望对你有帮助。 还是那句话:能力有限,不喜勿喷。