网上关于Casbin
的教程都比较麻烦,上手难度大,此篇仅供初学者学习,主要是将晦涩难懂的原理简单化,下面将分为8个知识点来应用Casbin
访问控制框架。这里不对Casbin
原理进行详细解释,如果想学习,请访问Casbin技术文档
前言
网上关于Casbin
的教程都比较麻烦,上手难度大,此篇仅供初学者学习,主要是将晦涩难懂的原理简单化,下面将分为8个知识点来应用Casbin
访问控制框架。这里不对Casbin
原理进行详细解释,如果想学习,请访问Casbin技术文档
名词理解
- Model : 权限模型(例如ACL、RBAC、ABAC等)
- Adapter : 链接框架和数据库的中间件
- request(r ) : 用户提供的信息
- policy(p ) : 规则提供的信息(基本上规则要什么,用户就要给什么)
- g(role) : 角色匹配( _ , _就表示前者继承后者)
- subject(sub) : 对象(用户名或者角色)
- object(obj) : 资源(路径,如
/user/1
) - action(act) : 操作(方式,如
GET
、POST
等) - Matchers : 规则(计算布尔值的计算公式)
1.安装
下载Casbin
的访问控制框架和beego-ORM-Adapter
go get github.com/casbin/casbin
go get github.com/casbin/beego-orm-adapter
实际上作者提供的beego-ORM-Adapter
包比较难用,需要自己重写。
2.配置
在conf
文件夹中新建casbin.conf
,内容如下:
conf/casbin.conf
[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) && keyMatch2(r.obj, p.obj) && regexMatch(r.act, p.act)
然后假设我们Beego已经注册好MySQL链接,如下所示:
err := orm.RegisterDataBase("default", "mysql", dbLink, maxIdle, maxConn)
那么我们就不需要beego-ORM-Adapter
原本提供的数据库注册,所以需要魔改。
3.Casbin数据库模型
先抛下魔改的部分,我们得先创建Casbin
对应的数据库表,这个表的建立很简单,直接写一个结构体,包含以下
models/Casbin.go
type CasbinRule struct {
Id int // 自增主键
PType string // Policy Type - 用于区分 policy和 group(role)
V0 string // subject
V1 string // object
V2 string // action
V3 string // 这个和下面的字段无用,仅预留位置,如果你的不是
V4 string // sub, obj, act的话才会用到
V5 string // 如 sub, obj, act, suf就会用到 V3
}
然后直接通过orm注册模型,并migrate到数据库中,这个和其他模型一样
models/Casbin.go
func init(){
orm.RegisterModel(new(CasbinRule))
_ = orm.RunSyncdb("default", false, false)
}
4.Adapter
为什么要自己修改一个
Adapter
?
我们看一下beego-ORM-Adapter
中NewAdapter
方法源码(部分)
func NewAdapter(driverName string, dataSourceName string, dbSpecified ...bool) *Adapter {
a := &Adapter{}
a.driverName = driverName
a.dataSourceName = dataSourceName
.....
// Open the DB, create it if not existed.
// 打开一个Database链接,如果不存在
a.open()
.....
return a
}
func (a *Adapter) open() {
.....
err = a.registerDataBase("default", a.driverName, a.dataSourceName)
if err != nil {
panic(err)
}
.....
a.o = orm.NewOrm()
a.o.Using("casbin")
a.createTable()
}
我们可以看到NewAdapter
调用了open
方法,open
方法又重新注册了一遍Database,然后再创建表,这可能与我们本身Beego项目写的数据库注册代码发生冲突。
通过源码分析,我们可以知道beego-ORM-Adapter
实际上就是定义了一个Adapter
结构体,里面有一个orm.Ormer
变量而已,最终使用的也只是这个变量。所以我们直接在自己项目中重新构建一个Adapter
结构体,然后重写一个初始化方法。
/models/Casbin.go
// 注意,这个Enforcer很重要,Casbin使用都是调用这个变量
var Enforcer *casbin.Enforcer
type Adapter struct {
o orm.Ormer
}
func RegisterCasbin() {
a := &Adapter{}
a.o = orm.NewOrm()
// 这个我不知道干嘛的
runtime.SetFinalizer(a, finalizer)
// Enforcer初始化 - 即传入Adapter对象
Enforcer = casbin.NewEnforcer("conf/casbin.conf", a)
// Enforcer读取Policy
err := Enforcer.LoadPolicy()
if err != nil {
panic(err)
}
}
// finalizer is the destructor for Adapter.
// 这个函数里面啥都没有,就是这样
func finalizer(_ *Adapter) {}
然后我们将beego-ORM-Adapter
中其他的方法复制过来,如下
/models/Casbin.go
// 注意,方法对应的具体代码要从beego-ORM-Adapter/adapter.go中复制过来
// 这里方法里面使用的orm操作还是要根据自己的
// 实际情况作出调整,不要盲目复制
func loadPolicyLine(line CasbinRule, model model.Model){}
func savePolicyLine(ptype string, rule []string) CasbinRule{}
func (a *Adapter) LoadPolicy(model model.Model) error{}
func (a *Adapter) SavePolicy(model model.Model) error{}
func (a *Adapter) AddPolicy(sec string, ptype string, rule []string) error{}
func (a *Adapter) RemovePolicy(sec string, ptype string, rule []string) error{}
func (a *Adapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error{}
然后修改savePolicyLine
方法如下所示(删掉第一和第二行,即删除调用a.dropTable()
和a.createTable()
):
func (a *Adapter) SavePolicy(model model.Model) error {
var lines []CasbinRule
for ptype, ast := range model["p"] {
for _, rule := range ast.Policy {
line := savePolicyLine(ptype, rule)
lines = append(lines, line)
}
}
for ptype, ast := range model["g"] {
for _, rule := range ast.Policy {
line := savePolicyLine(ptype, rule)
lines = append(lines, line)
}
}
_, err := a.o.InsertMulti(len(lines), lines)
return err
}
最后最重要的是调用RegisterCasbin()
方法进行初始化
/models/Casbin.go
func init(){
orm.RegisterModel(new(CasbinRule))
// 实际上同步数据库在整个Beego项目中只需要执行一次,如果
// 您在别的地方已经同步数据库,这里就不用在执行一次 RunSyncdb
_ = orm.RunSyncdb("default", false, false)
// 初始化 Casbin
RegisterCasbin()
}
初始化的目的就是获取一个可用的Enforce
,这是Casbin访问控制框架的核心,无论什么操作都离不开它。
5.Role角色模型
我采用角色控制访问Restful Api的方法,每个用户都有自己的角色,这是一对多(OneToMany)的关系。直接定义一个角色模型如下:
models/Role.go
type Role struct {
Id int `orm:"auto;pk" description:"角色序号" json:"role_id"`
Name string `orm:"unique" description:"角色名" json:"role_name"`
Users []*User `orm:"reverse(many)" description:"用户列表" json:"users"`
}
func init(){
orm.RegisterModel(new(Role))
// 这里因为在别的地方已经同步过数据库了,就不同步了
}
然后我们还得初始化最基本的三个角色,分别是管理员、用户、匿名(未登陆的用户都是匿名),这个根据你项目实际需求去初始化。
models/Role.go
var (
RoleAdmin = "admin"
RoleUser = "user"
RoleAnonymous = "anonymous"
RolesId = map[string]int{
RoleAdmin: -1,
RoleUser: -1,
RoleAnonymous: -1,
}
)
// 注册角色模型 - 初始化
func RegisterRoles() {
o := orm.NewOrm()
// 这里我通过遍历上面构建的一个字典来写入数据库
// 如果不愿意使用骚操作的话,直接写三个ReadOrCreate就好了
// GetRoleString方法是必须的
for key, _ := range RolesId {
_, id, err := o.ReadOrCreate(&Role{Name: GetRoleString(key)}, "Name")
if err != nil {
panic(err)
}
RolesId[key] = int(id)
}
}
// 这个方法主要用于在Name字段加个前缀role_
func GetRoleString(s string) string {
if strings.HasPrefix(s, "role_") {
return s
}
return fmt.Sprintf("role_%s", s)
}
这里解释一下为什么要在各个角色的Name
字段中加上前缀role_
,因为Casbin
中g(role)
是不会区分两个数据的来源,全部识别为字符串,例如:判断一个用户名为admin
的用户(user
),是否拥有管理员权限,如果根据用户名判断就会出现Bug,虽然可以通过手动强制根据用户角色判断,但是为了避免出现类似的错误,加上保险,这里还是在每个Name
字段前加上role_
或者你自定义的字符串前缀。
然后我们再来把三个角色添加到Casbin
数据表中:
models/Role.go
// 向Casbin添加角色继承策略规则
func AddRolesGroupPolicy() {
// 普通管理员继承用户
_ = Enforcer.AddGroupingPolicy(GetRoleString(RoleAdmin), GetRoleString(RoleUser))
// 用户继承匿名者
_ = Enforcer.AddGroupingPolicy(GetRoleString(RoleUser), GetRoleString(RoleAnonymous))
}
最后在初始化包的时候调用这两个方法:
func init(){
RegisterRoles()
AddRolesGroupPolicy()
}
6.User模型
这里定义的User
模型就是用户模型,包括用户名Username
、密码Password
、角色Role
三个字段,这个根据自己Beego项目实际情况决定,能看到这篇文章的兄弟实际上对这个已经不陌生了,属于基本操作。
models/User.go
type User struct {
// 用户模型
Id int `orm:"auto;pk" description:"用户序号" json:"uid"`
Username string `orm:"unique" description:"用户名" json:"username"`
Password string `description:"用户密码" json:"password"`
Role *Role `orm:"rel(fk);null" description:"角色" json:"Role"`
}
// 各种ORM查询方法请自行实现,这里不强调
7.定义用户控制器Controller
因为使用Casbin
对Restful Api
进行管理,所以Casbin
通常用在过滤器中。首先我们定义三个控制器,分别对应管理员
、用户
、匿名
可以访问。
controllers/User.go
type UserController struct {
beego.Controller
}
// 只有管理员才能注册
// @router /register [post]
func (c *UserController) Register(){}
// 只有用户、管理员才能看到别人或者自己的个人资料
// 因为管理员继承用户,所以用户能做到的,管理员也可
// @router /profile [get]
func (c *UserController) Profile(){}
// 匿名也能登陆
// @router /login [post]
func (c *UserController) Login(){}
然后我们还得把具体Policy
策略写入到数据库之中,下面的实现方法采用硬编码,是我目前能想到最好的方法了,如果有更好的方法,请留言,谢谢!
controllers/User.go
func registerUserPolicy() {
// Path前缀,这个根据具体项目自行调整
api := "/v1/user"
// 路由的Policy
adminPolicy := map[string][]string{
"/register": {"post"},
}
userPolicy := map[string][]string{
// 注意 - casbin.conf中使用 keyMatch2 对 obj 进行
// 验证,这里要使用 :id 来对参数进行标识
"/:id": {"get", "put", "delete"},
}
anonymousPolicy := map[string][]string{
"/login": {"post"},
}
// models.RoleAdmin = "admin"
// models.RoleUser = "user"
// models.RoleAnonymous = "anonymous"
AddPolicyFromController(models.RoleAdmin, adminPolicy, api)
AddPolicyFromController(models.RoleUser, userPolicy, api)
AddPolicyFromController(models.RoleAnonymous, anonymousPolicy, api)
}
func AddPolicyFromController(role string, policy map[string][]string, api string) {
for path := range policy {
for _, method := range policy[path] {
// models.Enforcer在models/Casbin.go中定义并初始化
_ = models.Enforcer.AddPolicy(models.GetRoleString(role), fmt.Sprintf("%s%s", api, path), method)
}
}
}
最后别忘了初始化调用方法:
controllers/User.go
func init(){
registerUserPolicy()
}
8.过滤器使用Casbin
访问控制
Beego
自带authz
模块插件用于支持Casbin
,但是感觉不怎么好用,写的不太灵活,还是要参照着authz
自己来重写。根据作者思想,先定一个BasicAuthorizer
结构体存放Enforcer
变量
filters/User.go(自己新建)
type BasicAuthorizer struct {
enforcer *casbin.Enforcer
}
然后我们是对用户角色进行验证,所以还要获取用户角色,因为我是在登陆时候把用户信息写入Session
之中,所以我直接读取Session
就可以获取:
filters/User.go
func (a *BasicAuthorizer) GetUserRole(input *context.BeegoInput) string {
user, ok := input.Session("user").(*models.User)
// 判断是否成功通过Session获取用户信息
if !ok || user.Role.Name == "" {
// 不成功的话直接返回匿名
return models.GetRoleString(models.RoleAnonymous)
}
return user.Role.Name
}
然后定义一个beego.FilterFunc
方法,这个方法中要使用Casbin
包提供的Enforce
方法对用户角色、访问路径、方式进行校验,方法返回布尔值
:
filters/User.go
func NewAuthorizer(e *casbin.Enforcer) beego.FilterFunc {
return func(ctx *context.Context) {
// 通过创建结构体,存放Enforcer
a := &BasicAuthorizer{enforcer: e}
// 获取用户角色
userRole := a.GetUserRole(ctx.Input)
// 获取访问路径
method := strings.ToLower(ctx.Request.Method)
// 获取访问方式
path := strings.ToLower(ctx.Request.URL.Path)
// 进行验证 - 失败则返回401
if status := a.enforcer.Enforce(userRole, path, method); !status {
ctx.Output.Status = 401
_ = ctx.Output.JSON(map[string]string{"msg": "用户权限不足"}, beego.BConfig.RunMode != "prod", false)
}
}
}
这里有一点需要提醒一下,Casbin
中对大小写敏感,建议统一使用小写或者大写,避免出现Bug。
最后我们还得把这个过滤器附加上:
main.go
func main() {
......
beego.InsertFilter("/v1/user/*", beego.BeforeRouter, filters.NewAuthorizer(models.Enforcer))
......
beego.Run()
}
9.总结
Casbin
这个开源框架虽然作者是国人,但是技术文档写的比较晦涩难懂,作为学生党的我好不容易能理解一下,用更浅显的方式写一个笔记,希望能够帮助到大家,非常感谢Casbin
作者Yang Luo。