權限管理在幾乎每一個系統中都是必備的模塊。若是項目開發每次都要實現一次權限管理,無疑會浪費開發時間,增長開發成本。所以,casbin
庫出現了。casbin
是一個強大、高效的訪問控制庫。支持經常使用的多種訪問控制模型,如ACL/RBAC/ABAC
等。能夠實現靈活的訪問權限控制。同時,casbin
支持多種編程語言,Go/Java/Node/PHP/Python/.NET/Rust
。咱們只須要一次學習,多處運用。html
咱們依然使用 Go Module 編寫代碼,先初始化:mysql
$ mkdir casbin && cd casbin
$ go mod init github.com/darjun/go-daily-lib/casbin
複製代碼
而後安裝casbin
,目前是v2
版本:git
$ go get github.com/casbin/casbin/v2
複製代碼
權限實際上就是控制誰能對什麼資源進行什麼操做。casbin
將訪問控制模型抽象到一個基於 PERM(Policy,Effect,Request,Matchers) 元模型的配置文件(模型文件)中。所以切換或更新受權機制只須要簡單地修改配置文件。github
policy
是策略或者說是規則的定義。它定義了具體的規則。golang
request
是對訪問請求的抽象,它與e.Enforce()
函數的參數是一一對應的sql
matcher
匹配器會將請求與定義的每一個policy
一一匹配,生成多個匹配結果。數據庫
effect
根據對請求運用匹配器得出的全部結果進行彙總,來決定該請求是容許仍是拒絕。編程
下面這張圖很好地描繪了這個過程:bash
咱們首先編寫模型文件:微信
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
[policy_effect]
e = some(where (p.eft == allow))
複製代碼
上面模型文件規定了權限由sub,obj,act
三要素組成,只有在策略列表中有和它徹底相同的策略時,該請求才能經過。匹配器的結果能夠經過p.eft
獲取,some(where (p.eft == allow))
表示只要有一條策略容許便可。
而後咱們策略文件(即誰能對什麼資源進行什麼操做):
p, dajun, data1, read
p, lizi, data2, write
複製代碼
上面policy.csv
文件的兩行內容表示dajun
對數據data1
有read
權限,lizi
對數據data2
有write
權限。
接下來就是使用的代碼:
package main
import (
"fmt"
"log"
"github.com/casbin/casbin/v2"
)
func check(e *casbin.Enforcer, sub, obj, act string) {
ok, _ := e.Enforce(sub, obj, act)
if ok {
fmt.Printf("%s CAN %s %s\n", sub, act, obj)
} else {
fmt.Printf("%s CANNOT %s %s\n", sub, act, obj)
}
}
func main() {
e, err := casbin.NewEnforcer("./model.conf", "./policy.csv")
if err != nil {
log.Fatalf("NewEnforecer failed:%v\n", err)
}
check(e, "dajun", "data1", "read")
check(e, "lizi", "data2", "write")
check(e, "dajun", "data1", "write")
check(e, "dajun", "data2", "read")
}
複製代碼
代碼其實不復雜。首先建立一個casbin.Enforcer
對象,加載模型文件model.conf
和策略文件policy.csv
,調用其Enforce
方法來檢查權限。運行程序:
$ go run main.go
dajun CAN read data1
lizi CAN write data2
dajun CANNOT write data1
dajun CANNOT read data2
複製代碼
請求必須徹底匹配某條策略才能經過。("dajun", "data1", "read")
匹配p, dajun, data1, read
,("lizi", "data2", "write")
匹配p, lizi, data2, write
,因此前兩個檢查經過。第 3 個由於"dajun"
沒有對data1
的write
權限,第 4 個由於dajun
對data2
沒有read
權限,因此檢查都不能經過。輸出結果符合預期。
sub/obj/act
依次對應傳給Enforce
方法的三個參數。實際上這裏的sub/obj/act
和read/write/data1/data2
是我本身隨便取的,你徹底可使用其它的名字,只要能先後一致便可。
上面例子中實現的就是ACL
(access-control-list,訪問控制列表)。ACL
顯示定義了每一個主體對每一個資源的權限狀況,未定義的就沒有權限。咱們還能夠加上超級管理員,超級管理員能夠進行任何操做。假設超級管理員爲root
,咱們只須要修改匹配器:
[matchers]
e = r.sub == p.sub && r.obj == p.obj && r.act == p.act || r.sub == "root"
複製代碼
只要訪問主體是root
一概放行。
驗證:
func main() {
e, err := casbin.NewEnforcer("./model.conf", "./policy.csv")
if err != nil {
log.Fatalf("NewEnforecer failed:%v\n", err)
}
check(e, "root", "data1", "read")
check(e, "root", "data2", "write")
check(e, "root", "data1", "execute")
check(e, "root", "data3", "rwx")
}
複製代碼
由於sub = "root"
時,匹配器必定能經過,運行結果:
$ go run main.go
root CAN read data1
root CAN write data2
root CAN execute data1
root CAN rwx data3
複製代碼
ACL
模型在用戶和資源都比較少的狀況下沒什麼問題,可是用戶和資源量一大,ACL
就會變得異常繁瑣。想象一下,每次新增一個用戶,都要把他須要的權限從新設置一遍是多麼地痛苦。RBAC
(role-based-access-control)模型經過引入角色(role
)這個中間層來解決這個問題。每一個用戶都屬於一個角色,例如開發者、管理員、運維等,每一個角色都有其特定的權限,權限的增長和刪除都經過角色來進行。這樣新增一個用戶時,咱們只須要給他指派一個角色,他就能擁有該角色的全部權限。修改角色的權限時,屬於這個角色的用戶權限就會相應的修改。
在casbin
中使用RBAC
模型須要在模型文件中添加role_definition
模塊:
[role_definition]
g = _, _
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
複製代碼
g = _,_
定義了用戶——角色,角色——角色的映射關係,前者是後者的成員,擁有後者的權限。而後在匹配器中,咱們不須要判斷r.sub
與p.sub
徹底相等,只須要使用g(r.sub, p.sub)
來判斷請求主體r.sub
是否屬於p.sub
這個角色便可。最後咱們修改策略文件添加用戶——角色定義:
p, admin, data, read
p, admin, data, write
p, developer, data, read
g, dajun, admin
g, lizi, developer
複製代碼
上面的policy.csv
文件規定了,dajun
屬於admin
管理員,lizi
屬於developer
開發者,使用g
來定義這層關係。另外admin
對數據data
用read
和write
權限,而developer
對數據data
只有read
權限。
package main
import (
"fmt"
"log"
"github.com/casbin/casbin/v2"
)
func check(e *casbin.Enforcer, sub, obj, act string) {
ok, _ := e.Enforce(sub, obj, act)
if ok {
fmt.Printf("%s CAN %s %s\n", sub, act, obj)
} else {
fmt.Printf("%s CANNOT %s %s\n", sub, act, obj)
}
}
func main() {
e, err := casbin.NewEnforcer("./model.conf", "./policy.csv")
if err != nil {
log.Fatalf("NewEnforecer failed:%v\n", err)
}
check(e, "dajun", "data", "read")
check(e, "dajun", "data", "write")
check(e, "lizi", "data", "read")
check(e, "lizi", "data", "write")
}
複製代碼
很顯然lizi
所屬角色沒有write
權限:
dajun CAN read data
dajun CAN write data
lizi CAN read data
lizi CANNOT write data
複製代碼
RBAC
casbin
支持同時存在多個RBAC
系統,即用戶和資源都有角色:
[role_definition]
g=_,_
g2=_,_
[matchers]
m = g(r.sub, p.sub) && g2(r.obj, p.obj) && r.act == p.act
複製代碼
上面的模型文件定義了兩個RBAC
系統g
和g2
,咱們在匹配器中使用g(r.sub, p.sub)
判斷請求主體屬於特定組,g2(r.obj, p.obj)
判斷請求資源屬於特定組,且操做一致便可放行。
策略文件:
p, admin, prod, read
p, admin, prod, write
p, admin, dev, read
p, admin, dev, write
p, developer, dev, read
p, developer, dev, write
p, developer, prod, read
g, dajun, admin
g, lizi, developer
g2, prod.data, prod
g2, dev.data, dev
複製代碼
先看角色關係,即最後 4 行,dajun
屬於admin
角色,lizi
屬於developer
角色,prod.data
屬於生產資源prod
角色,dev.data
屬於開發資源dev
角色。admin
角色擁有對prod
和dev
類資源的讀寫權限,developer
只能擁有對dev
的讀寫權限和prod
的讀權限。
check(e, "dajun", "prod.data", "read")
check(e, "dajun", "prod.data", "write")
check(e, "lizi", "dev.data", "read")
check(e, "lizi", "dev.data", "write")
check(e, "lizi", "prod.data", "write")
複製代碼
第一個函數中e.Enforce()
方法在實際執行的時候先獲取dajun
所屬角色admin
,再獲取prod.data
所屬角色prod
,根據文件中第一行p, admin, prod, read
容許請求。最後一個函數中lizi
屬於角色developer
,而prod.data
屬於角色prod
,全部策略都不容許,故該請求被拒絕:
dajun CAN read prod.data
dajun CAN write prod.data
lizi CAN read dev.data
lizi CAN write dev.data
lizi CANNOT write prod.data
複製代碼
casbin
還能爲角色定義所屬角色,從而實現多層角色關係,這種權限關係是能夠傳遞的。例如dajun
屬於高級開發者senior
,seinor
屬於開發者,那麼dajun
也屬於開發者,擁有開發者的全部權限。咱們能夠定義開發者共有的權限,而後額外爲senior
定義一些特殊的權限。
模型文件不用修改,策略文件改動以下:
p, senior, data, write
p, developer, data, read
g, dajun, senior
g, senior, developer
g, lizi, developer
複製代碼
上面policy.csv
文件定義了高級開發者senior
對數據data
有write
權限,普通開發者developer
對數據只有read
權限。同時senior
也是developer
,因此senior
也繼承其read
權限。dajun
屬於senior
,因此dajun
對data
有read
和write
權限,而lizi
只屬於developer
,對數據data
只有read
權限。
check(e, "dajun", "data", "read")
check(e, "dajun", "data", "write")
check(e, "lizi", "data", "read")
check(e, "lizi", "data", "write")
複製代碼
RBAC
domain在casbin
中,角色能夠是全局的,也能夠是特定domain
(領域)或tenant
(租戶),能夠簡單理解爲組。例如dajun
在組tenant1
中是管理員,擁有比較高的權限,在tenant2
可能只是個弟弟。
使用RBAC domain
須要對模型文件作如下修改:
[request_definition]
r = sub, dom, obj, act
[policy_definition]
p = sub, dom, obj, act
[role_definition]
g = _,_,_
[matchers]
m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.obj
複製代碼
g=_,_,_
表示前者在後者中擁有中間定義的角色,在匹配器中使用g
要帶上dom
。
p, admin, tenant1, data1, read
p, admin, tenant2, data2, read
g, dajun, admin, tenant1
g, dajun, developer, tenant2
複製代碼
在tenant1
中,只有admin
能夠讀取數據data1
。在tenant2
中,只有admin
能夠讀取數據data2
。dajun
在tenant1
中是admin
,可是在tenant2
中不是。
func check(e *casbin.Enforcer, sub, domain, obj, act string) {
ok, _ := e.Enforce(sub, domain, obj, act)
if ok {
fmt.Printf("%s CAN %s %s in %s\n", sub, act, obj, domain)
} else {
fmt.Printf("%s CANNOT %s %s in %s\n", sub, act, obj, domain)
}
}
func main() {
e, err := casbin.NewEnforcer("./model.conf", "./policy.csv")
if err != nil {
log.Fatalf("NewEnforecer failed:%v\n", err)
}
check(e, "dajun", "tenant1", "data1", "read")
check(e, "dajun", "tenant2", "data2", "read")
}
複製代碼
結果不出意料:
dajun CAN read data1 in tenant1
dajun CANNOT read data2 in tenant2
複製代碼
RBAC
模型對於實現比較規則的、相對靜態的權限管理很是有用。可是對於特殊的、動態的需求,RBAC
就顯得有點力不從心了。例如,咱們在不一樣的時間段對數據data
實現不一樣的權限控制。正常工做時間9:00-18:00
全部人均可以讀寫data
,其餘時間只有數據全部者能讀寫。這種需求咱們能夠很方便地使用ABAC
(attribute base access list)模型完成:
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[matchers]
m = r.sub.Hour >= 9 && r.sub.Hour < 18 || r.sub.Name == r.obj.Owner
[policy_effect]
e = some(where (p.eft == allow))
複製代碼
該規則不須要策略文件:
type Object struct {
Name string
Owner string
}
type Subject struct {
Name string
Hour int
}
func check(e *casbin.Enforcer, sub Subject, obj Object, act string) {
ok, _ := e.Enforce(sub, obj, act)
if ok {
fmt.Printf("%s CAN %s %s at %d:00\n", sub.Name, act, obj.Name, sub.Hour)
} else {
fmt.Printf("%s CANNOT %s %s at %d:00\n", sub.Name, act, obj.Name, sub.Hour)
}
}
func main() {
e, err := casbin.NewEnforcer("./model.conf", "./policy.csv")
if err != nil {
log.Fatalf("NewEnforecer failed:%v\n", err)
}
o := Object{"data", "dajun"}
s1 := Subject{"dajun", 10}
check(e, s1, o, "read")
s2 := Subject{"lizi", 10}
check(e, s2, o, "read")
s3 := Subject{"dajun", 20}
check(e, s3, o, "read")
s4 := Subject{"lizi", 20}
check(e, s4, o, "read")
}
複製代碼
顯然lizi
在20:00
不能read
數據data
:
dajun CAN read data at 10:00
lizi CAN read data at 10:00
dajun CAN read data at 20:00
lizi CANNOT read data at 20:00
複製代碼
咱們知道,在model.conf
文件中能夠經過r.sub
和r.obj
,r.act
來訪問傳給Enforce
方法的參數。實際上sub/obj
能夠是結構體對象,得益於govaluate
庫的強大功能,咱們能夠在model.conf
文件中獲取這些結構體的字段值。如上面的r.sub.Name
、r.Obj.Owner
等。govaluate
庫的內容能夠參見我以前的一篇文章《Go 每日一庫之 govaluate》。
使用ABAC
模型能夠很是靈活的權限控制,可是通常狀況下RBAC
就已經夠用了。
上面代碼中,咱們一直將模型存儲在文件中。casbin
也能夠實如今代碼中動態初始化模型,例如get-started
的例子能夠改寫爲:
func main() {
m := model.NewModel()
m.AddDef("r", "r", "sub, obj, act")
m.AddDef("p", "p", "sub, obj, act")
m.AddDef("e", "e", "some(where (p.eft == allow))")
m.AddDef("m", "m", "r.sub == g.sub && r.obj == p.obj && r.act == p.act")
a := fileadapter.NewAdapter("./policy.csv")
e, err := casbin.NewEnforcer(m, a)
if err != nil {
log.Fatalf("NewEnforecer failed:%v\n", err)
}
check(e, "dajun", "data1", "read")
check(e, "lizi", "data2", "write")
check(e, "dajun", "data1", "write")
check(e, "dajun", "data2", "read")
}
複製代碼
一樣地,咱們也能夠從字符串中加載模型:
func main() {
text := ` [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 && r.obj == p.obj && r.act == p.act `
m, _ := model.NewModelFromString(text)
a := fileadapter.NewAdapter("./policy.csv")
e, _ := casbin.NewEnforcer(m, a)
check(e, "dajun", "data1", "read")
check(e, "lizi", "data2", "write")
check(e, "dajun", "data1", "write")
check(e, "dajun", "data2", "read")
}
複製代碼
可是這兩種方式並不推薦。
在前面的例子中,咱們都是將策略存儲在policy.csv
文件中。通常在實際應用中,不多使用文件存儲。casbin
以第三方適配器的方式支持多種存儲方式包括MySQL/MongoDB/Redis/Etcd
等,還能夠實現本身的存儲。完整列表看這裏casbin.org/docs/en/ada…。下面咱們介紹使用Gorm Adapter
。先鏈接到數據庫,執行下面的SQL
:
CREATE DATABASE IF NOT EXISTS casbin;
USE casbin;
CREATE TABLE IF NOT EXISTS casbin_rule (
p_type VARCHAR(100) NOT NULL,
v0 VARCHAR(100),
v1 VARCHAR(100),
v2 VARCHAR(100),
v3 VARCHAR(100),
v4 VARCHAR(100),
v5 VARCHAR(100)
);
INSERT INTO casbin_rule VALUES
('p', 'dajun', 'data1', 'read', '', '', ''),
('p', 'lizi', 'data2', 'write', '', '', '');
複製代碼
而後使用Gorm Adapter
加載policy
,Gorm Adapter
默認使用casbin
庫中的casbin_rule
表:
package main
import (
"fmt"
"github.com/casbin/casbin/v2"
gormadapter "github.com/casbin/gorm-adapter/v2"
_ "github.com/go-sql-driver/mysql"
)
func check(e *casbin.Enforcer, sub, obj, act string) {
ok, _ := e.Enforce(sub, obj, act)
if ok {
fmt.Printf("%s CAN %s %s\n", sub, act, obj)
} else {
fmt.Printf("%s CANNOT %s %s\n", sub, act, obj)
}
}
func main() {
a, _ := gormadapter.NewAdapter("mysql", "root:12345@tcp(127.0.0.1:3306)/")
e, _ := casbin.NewEnforcer("./model.conf", a)
check(e, "dajun", "data1", "read")
check(e, "lizi", "data2", "write")
check(e, "dajun", "data1", "write")
check(e, "dajun", "data2", "read")
}
複製代碼
運行:
dajun CAN read data1
lizi CAN write data2
dajun CANNOT write data1
dajun CANNOT read data2
複製代碼
咱們能夠在匹配器中使用函數。casbin
內置了一些函數keyMatch/keyMatch2/keyMatch3/keyMatch4
都是匹配 URL 路徑的,regexMatch
使用正則匹配,ipMatch
匹配 IP 地址。參見casbin.org/docs/en/fun…。使用內置函數咱們能很容易對路由進行權限劃分:
[matchers]
m = r.sub == p.sub && keyMatch(r.obj, p.obj) && r.act == p.act
複製代碼
p, dajun, user/dajun/*, read
p, lizi, user/lizi/*, read
複製代碼
不一樣用戶只能訪問其對應路由下的 URL:
func main() {
e, err := casbin.NewEnforcer("./model.conf", "./policy.csv")
if err != nil {
log.Fatalf("NewEnforecer failed:%v\n", err)
}
check(e, "dajun", "user/dajun/1", "read")
check(e, "lizi", "user/lizi/2", "read")
check(e, "dajun", "user/lizi/1", "read")
}
複製代碼
輸出:
dajun CAN read user/dajun/1
lizi CAN read user/lizi/2
dajun CANNOT read user/lizi/1
複製代碼
咱們固然也能夠定義本身的函數。先定義一個函數,返回 bool:
func KeyMatch(key1, key2 string) bool {
i := strings.Index(key2, "*")
if i == -1 {
return key1 == key2
}
if len(key1) > i {
return key1[:i] == key2[:i]
}
return key1 == key2[:i]
}
複製代碼
這裏實現了一個簡單的正則匹配,只處理*
。
而後將這個函數用interface{}
類型包裝一層:
func KeyMatchFunc(args ...interface{}) (interface{}, error) {
name1 := args[0].(string)
name2 := args[1].(string)
return (bool)(KeyMatch(name1, name2)), nil
}
複製代碼
而後添加到權限認證器中:
e.AddFunction("my_func", KeyMatchFunc)
複製代碼
這樣咱們就能夠在匹配器中使用該函數實現正則匹配了:
[matchers]
m = r.sub == p.sub && my_func(r.obj, p.obj) && r.act == p.act
複製代碼
接下來咱們在策略文件中爲dajun
賦予權限:
p, dajun, data/*, read
複製代碼
dajun
對匹配模式data/*
的文件都有read
權限。
驗證一下:
check(e, "dajun", "data/1", "read")
check(e, "dajun", "data/2", "read")
check(e, "dajun", "data/1", "write")
check(e, "dajun", "mydata", "read")
複製代碼
dajun
對data/1
沒有write
權限,mydata
不符合data/*
模式,也沒有read
權限:
dajun CAN read data/1
dajun CAN read data/2
dajun CANNOT write data/1
dajun CANNOT read mydata
複製代碼
casbin
功能強大,簡單高效,且多語言通用。值得學習。
你們若是發現好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue😄
個人博客:darjun.github.io
歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~