Golang Beego使用Casbin进行Restful Api权限管理教程

5 篇文章 0 订阅
1 篇文章 0 订阅

Beego使用Casbin进行权限管理(MySQL)入门


前言

网上关于Casbin的教程都比较麻烦,上手难度大,此篇仅供初学者学习,主要是将晦涩难懂的原理简单化,下面将分为8个知识点来应用Casbin访问控制框架。这里不对Casbin原理进行详细解释,如果想学习,请访问Casbin技术文档


名词理解

  • Model : 权限模型(例如ACL、RBAC、ABAC等)
  • Adapter : 链接框架和数据库的中间件
  • request(r ) : 用户提供的信息
  • policy(p ) : 规则提供的信息(基本上规则要什么,用户就要给什么)
  • g(role) : 角色匹配( _ , _就表示前者继承后者)
  • subject(sub) : 对象(用户名或者角色)
  • object(obj) : 资源(路径,如/user/1
  • action(act) : 操作(方式,如GETPOST等)
  • 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-AdapterNewAdapter方法源码(部分)

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_,因为Casbing(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

因为使用CasbinRestful 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。

  • 8
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
好的,我会尽力回答您的问题。首先,您需要安装 Go 和 Beego,以及 go-socket.io 库。可以使用以下命令安装 go-socket.io: ``` go get github.com/googollee/go-socket.io ``` 接下来,您需要创建一个 Beego 项目。可以使用以下命令创建一个名为 myproject 的项目: ``` bee new myproject ``` 然后,您需要在 main.go 文件中引入 go-socket.io 库,并创建一个 Socket.IO 服务器: ```go import ( "github.com/astaxie/beego" "github.com/googollee/go-socket.io" ) func main() { // 创建 Beego 应用 app := beego.NewApp() // 创建 Socket.IO 服务器 server, err := socketio.NewServer(nil) if err != nil { beego.Error(err) } defer server.Close() // 注册事件处理函数 server.On("connection", func(so socketio.Socket) { beego.Info("on connection") // 处理客户端连接事件 }) // 将 Socket.IO 服务器挂载到 Beego 应用 app.Router("/socket.io/", &SocketIOController{server}) beego.Run() } // SocketIOController 是一个 Beego 控制器,用于处理 Socket.IO 请求 type SocketIOController struct { server *socketio.Server } func (c *SocketIOController) Get() { c.server.ServeHTTP(c.Ctx.ResponseWriter, c.Ctx.Request) } ``` 最后,您需要在前端页面中使用 Socket.IO 客户端库来连接服务器并处理事件。以下是一个简单的示例: ```html <!DOCTYPE html> <html> <head> <title>Socket.IO Example</title> <script src="/socket.io/socket.io.js"></script> <script> var socket = io.connect(); socket.on('connect', function() { console.log('connected'); }); socket.on('message', function(data) { console.log('message received:', data); }); socket.emit('message', 'hello'); </script> </head> <body> <h1>Socket.IO Example</h1> </body> </html> ``` 这是一个简单的示例,您可以根据自己的需求进行修改和扩展。希望能对您有所帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值