背景交代
RBAC数据库表结构模型如下图:
很简单的一个数据库模型,系统资源表存储系统功能url及名称等信息,角色与用户是多对多,即一个用户可以有多种角色,角色与资源表多对多,即多个角色中每一种可操作的系统资源可以各不相同。
casbin
casbin有针对数据库的adapter,它自己也能读取数据库内容,但是没细研究是否需要建立适配casbin的那一套结构,毕竟大家对rbac模型太熟悉了,决定不用casbin自己的表结构,把以上的数据组织成满足casbin规则的数据给它去用就行了。
要使用casbin必须先定义一个模型文件,文件内容用于说明使用哪一种权限模型,这里使用的权限模型如下:
rbac.conf
[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 && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*")
注意最后一行的p.act="*",这个意思是说忽略客户端浏览器发起的请求类型,即不分辨GET、POST、FETCH等http方法。
以上是一个改动过的rbac权限模型,然后初始化casbin还需要一个具体写有权限内容的文件,我这里就给提供个空文件就完事了,因为规则是在数据库里面配置的,用以下代码实例化casbin对象,代码中的rbac_policy.csv文件就是个没有内容的空文件。
e := casbin.NewEnforcer("./configs/rbac.conf", "./configs/rbac_policy.csv")
在gin框架中要使用的话这么写中间件
router := gin.Default()
web_router:=router.Group("/web")
web_router.Use(checkLoginHandle(e))
func checkLoginHandle(e *casbin.Enforcer) gin.HandlerFunc {
return func(c *gin.Context) {
//获取请求的URI
obj_uri := c.Request.URL.RequestURI()
if strings.HasPrefix(obj_uri,"/web/login") || strings.HasPrefix(obj_uri,"/web/assets"){
c.Next()//放行
return
}
//获取请求方法
act := c.Request.Method
//获取用户的角色
session := sessions.Default(c)
if nil==session.Get("current_role_code"){
//session中取不到当前选中的角色,未登录?
c.Redirect(http.StatusTemporaryRedirect,"/web/login")
return
}
current_user_role:=session.Get("current_role_code").(string) //从session取出当前用户选中的角色
sub :=current_user_role
//判断策略中是否存在
if e.Enforce(sub, obj_uri,act) {
fmt.Println("通过权限")
c.Next()
} else {
fmt.Println("权限没有通过")
c.Abort()
}
}
}
有了以上的代码,gin框架就可以使用casbin做验证了,下面的问题是怎么把自己设计的rbac数据规则给casbin。
核心代码其实就一句
e.AddPolicy(string(row["role_code"]),string(row["res_url"]), "*")
大体流程是写sql去联合查询角色表、角色与资源中间表、资源表三个表,得到每一种角色可以操作哪些系统资源,然后调用上面的代码把角色编码、资源地址加入到casbin里面去就完事了,最后那个*代表不区分GET还是POST还是DELETE等http method。具体什么时候调用自己控制,总之把校验规则给casbin就行了,后续注意调整角色权限或者资源信息时要更新这些规则。
以上是对客户端请求的url路径使用casbin加rbac权限模型进行校验,对html内容也可以用casbin进行内容展示控制,例如我想控制登录用户有权限的话才会显示某些功能的入口连接,可以这么整:
type CurrentLoginUserRole struct {
HasPermission func(string) bool
}
func (this *IndexController) index(c *gin.Context) {
session := sessions.Default(c)
if nil==session.Get("current_role_code"){
//未登录?
c.Redirect(http.StatusTemporaryRedirect,"/web/login")
return
}
current_user_role:=session.Get("current_role_code").(string)
c.HTML(http.StatusOK, "index.html", gin.H{
"user":CurrentLoginUserRole{
HasPermission: func(sys_res_id string) bool {
return casbinObj.Enforce(current_user_role,sys_res_id,"GET")
},
},
})
}
上面代码里面的casbinObj就是前面初始化的那个e,我是找了个全局变量casbinObj存起来了。
然后在index.html里面
<% if (call .user.HasPermission "/web/sysusers/default") %>
<li>
<a _href="/web/user/default">
<i class="iconfont"></i>
<cite>用户管理</cite>
</a>
</li >
<% end %>
至此,浏览器请求url路径时有gin的中间件做拦截校验,html页面内容也是不同的角色显示的各不相同,想实现到按钮级别的控制的话把按钮的资源地址配置到系统资源表里面,再在html页面里面对url和按钮元素可见性进行控制即可。
补充,以上是对角色进行的校验,不是对用户,需要用户登录成功以后先选择一个接下来操作系统的角色,因为是假定的一个登录用户是可能有多重角色的,校验的关键都是角色编码而不是用户编码,如果系统模型是一个用户最多只能有一种角色的话登录后直接把用户角色查出来省掉选择角色的步骤就行了。