Golang Beego使用Casbin進行Restful Api權限管理教程

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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章