casbin学习
一、背景
1. Casbin是什么
Casbin 是一个授权库,在我们希望特定用户访问特定的 对象 或实体的流程中可以使用 主题 访问类型,例如 动作 可以是 读取, 写入, 删除 或开发者设置的任何其他动作。 这是Casbin最广泛的使用,它叫做"标准" 或经典 { subject, object, action } 流程。
Casbin能够处理除标准流量以外的许多复杂的许可使用者。 可以添加 角色 (RBAC), 属性 (ABAC) 等。
2. 功能
- 支持自定义请求的格式,默认的请求格式为{ subject, object, action }。
- 具有访问控制模型model和策略policy两个核心概念。
- 支持RBAC中的多层角色继承,不止主体可以有角色,资源也可以具有角色。
- 支持内置超级用户,例如 root 或 管理员。 超级用户可以在没有明确权限的情况下做任何事情。
- 支持规则匹配的多个内置运营商。 例如, keyMatch 可以映射 资源密钥 /fo/bar to the pattern /foo*。
3. 不支持的功能
- 身份认证 authentication(即验证用户的用户名和密码),Casbin 只负责访问控制。应该有其他专门的组件负责身份认证,然后由 Casbin 进行访问控制,二者是相互配合的关系。
- 管理用户列表或角色列表。
4. PERM模型
**PERM(Policy, Effect, Request, Matchers)模型很简单, 但是反映了权限的本质 – 访问控制。**在 Casbin 中, 访问控制模型被抽象为基于 PERM (Policy, Effect, Request, Matcher) 的一个文件。 因此,切换或升级项目的授权机制与修改配置一样简单。 您可以通过组合可用的模型来定制您自己的访问控制模型。 例如,您可以在一个model中结合RBAC角色和ABAC属性,并共享一组policy规则。
-
policy: 定义权限的规则
- 策略:定义访问策略的模式。 事实上,它在策略规则文件中界定了字段的名称和顺序。
- 例如: p={sub, obj, act} 或 p={sub, obj, act, eft}
- 注:如果未定义eft (policy result),则策略文件中的结果字段将不会被读取, 和匹配的策略结果将默认被允许。
-
Effect: 定义组合了多个 Policy 之后的结果, allow/deny
- 效果。它可以被理解为一种模型,在这种模型中,对匹配结果再次作出逻辑组合判断。
- 例如: e = some (where (p.eft == allow)) 这句话意味着,如果匹配的策略结果有一些是允许的,那么最终结果为真。
- 让我们看看另一个示例: e = some (where (p.eft == allow)) && !some(where (p.eft == deny) 此示例组合的逻辑含义是:如果有符合允许结果的策略且没有符合拒绝结果的策略, 结果是为真。 换言之,当匹配策略均为允许(没有任何否认)是为真(更简单的是,既允许又同时否认,拒绝就具有优先地位)。
-
Request: 访问请求, 也就是谁想操作什么
- 请求。定义请求参数。 定义请求参数。基本请求是一个元组对象,至少需要主题(访问实体)、对象(访问资源) 和动作(访问方式)
- 例如:一个请求可能长这样: r={sub,obj,act}它实际上定义了我们应该提供访问控制匹配功能的参数名称和顺序。
-
Matcher: 判断 Request 是否满足 Policy.
- 匹配器。匹配请求和策略的规则。
- 例如: m = r.sub == p.sub && r.act == p.act && r.obj == p.obj 这个简单和常见的匹配规则意味着如果请求的参数(访问实体,访问资源和访问方式)匹配, 如果可以在策略中找到资源和方法,那么策略结果(p.eft)便会返回。 策略的结果将保存在 p.eft 中。
二、model语法
三、适配器
-
适配器作用
- 在Casbin中,策略存储作为adapter(Casbin的中间件) 实现。 Casbin用户可以使用adapter从存储中加载策略规则 (aka LoadPolicy()) 或者将策略规则保存到其中 (aka SavePolicy())。 为了保持代码轻量级,我们没有把adapter代码放在主库中。
-
适配器对应版本
四、实战
实现步骤
1. 选择权限模型
- ACL: RESTFUL权限模型
- 如 /res/*, /res/: id 和 HTTP 方法, 如 GET, POST, PUT, DELETE。
- 即什么角色,可以访问什么接口
2. 创建model.config数据
-
自定义的策略
3. 策略数据保存到mysql
4. 使用gin框架集成casbin
5. 编写auth中间件完成授权
直接看代码
-
settings.go
-
模拟一些数据
// 模拟用户 var Users = map[string]User{ "1":{ Name: "zhangsan", Age: "20", RoleKey: admin, }, "2":{ Name: "lisi", Age: "21", RoleKey: formalMember, }, "3":{ Name: "wangwu", Age: "23", RoleKey: businessAdmin, }, "4":{ Name: "zhaoliu", Age: "24", RoleKey: normalUser, }, "5":{ Name: "tianqi", Age: "25", RoleKey: formalMember, }, } //模拟认证 func GetIdentity(c *gin.Context) *User { auth := c.Request.Header.Get("Authentication") user,ok := Users[auth] if ok { return &user } return nil }
-
-
mycasbin.go
-
定义casbin restful权限模型,将策略存在数据库
/** 采用模型 RESTFUL 支持路径, 如 /res/*, /res/: id 和 HTTP 方法, 如 GET, POST, PUT, DELETE。 keyMatch2和keyMatch区别: keyMatch : 一个URL 路径或 * 模式下,例如 /alice_data/* keyMatch2:一个URL 路径或 : 模式下,例如 /alice_data/:resource r : 请求。 sub主题(访问实体)、obj对象(访问资源) 和 act动作(访问方式 p : 策略。 sub主题(访问实体)、obj对象(访问资源) 和 act动作(访问方式 m: 匹配器。请求访问实体 == 策略定义的访问实体 并且 (url匹配到请求对应中访问资源 || url匹配到请求和策略中对应的访问资源 ) 并且 (请求中的行为与策略中的行为一致 || 策略的行为*) e : 效果。 满足匹配器即为true,否则为false */ var 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 && (keyMatch2(r.obj, p.obj) || keyMatch(r.obj, p.obj)) && (r.act == p.act || p.act == "*") ` // 设置casbin func Setup(db *gorm.DB) (*casbin.SyncedEnforcer, error) { //建立数据库连接 //apter, err := gormadapter.NewAdapter("mysql", "root:root1234@tcp(127.0.0.1:31234)/") // Your driver and data source. apter, err := gormadapter.NewAdapterByDB(db) if err != nil { return nil, err } //将casbin model转化为字符串格式 modelFromString, err := model.NewModelFromString(text) if err != nil { return nil, err } //NewSyncedEnforcer通过文件或数据库创建同步执行器。 nef, err := casbin.NewSyncedEnforcer(modelFromString, apter) if err != nil { return nil, err } //LoadPolicy从文件/数据库重新加载策略。 err = nef.LoadPolicy() if err != nil { return nil, err } return nef, err
}
```-
此时会在数据库中初始化一张casbin_rule的表,表是空的。长这个样子
id ptype v0 v1 v2 v3 v4 v5 -
定义完api,然后手动创建数据
-
-
application.go
-
抽象配置
import ( "net/http" "sort" "strings" "sync" "github.com/casbin/casbin/v2" "github.com/gin-gonic/gin" "gorm.io/gorm" ) var Runtime = NewApplication() type Application struct { mux sync.RWMutex db *gorm.DB casbin *casbin.SyncedEnforcer } // SetDb 设置对应key的db func (e *Application) SetDb(db *gorm.DB) { e.mux.Lock() defer e.mux.Unlock() e.db = db } // GetDb 获取所有map里的db数据 func (e *Application) GetDb() *gorm.DB { e.mux.Lock() defer e.mux.Unlock() return e.db } func (e *Application) SetCasbin(enforcer *casbin.SyncedEnforcer) { e.mux.Lock() defer e.mux.Unlock() e.casbin = enforcer } func (e *Application) GetCasbin() *casbin.SyncedEnforcer { e.mux.Lock() defer e.mux.Unlock() return e.casbin } func NewApplication() Application { return Application{} }
-
-
middleware.go
-
模拟认证授权中间件
import ( "fmt" "github.com/gin-gonic/gin" "net/http" ) // 定义一些角色 TODO 未来数据库存储 const ( admin = "admin" businessAdmin = "businessAdmin" formalMember = "formalMember" normalUser = "normalUser" ) // 检查角色,鉴权 func AuthCheckRole() gin.HandlerFunc { return func(c *gin.Context) { // TODO 认证 jwt user := GetIdentity(c) if user == nil { c.JSON(http.StatusUnauthorized, gin.H{ "code": 401, "msg": "token过期,不存在", }) c.Abort() return } // 鉴权 casbin if user.RoleKey == admin { fmt.Printf("用户:%s, 是 %s, 直接通过! \n",user.Name,user.RoleKey) c.Next() return } // 数据库匹配 res, err := Runtime.casbin.Enforce(user.RoleKey, c.Request.URL.Path, c.Request.Method) if err != nil { fmt.Printf("AuthCheckRole error: %s method:%s path:%s\n", err, c.Request.Method, c.Request.URL.Path) c.JSON(http.StatusOK, gin.H{ "code": 500, "msg": err.Error(), }) return } // 匹配成功 if res { fmt.Printf("username :%s, isTrue: %v, role: %s method: %s path: %s \n", user.Name,res, user.RoleKey, c.Request.Method, c.Request.URL.Path) c.Next() } else { fmt.Printf("username :%s,isTrue: %v, role: %s method: %s path: %s message: %s \n", user.Name,res, user.RoleKey, c.Request.Method, c.Request.URL.Path, "当前request无权限,请管理员确认!") c.JSON(http.StatusForbidden, gin.H{ "code": 403, "msg": "对不起,您没有该接口访问权限,请联系管理员", }) c.Abort() return } } }
-
-
mycasbin_test.go
-
写一些测试case
package casbin import ( "fmt" "github.com/gin-gonic/gin" "gorm.io/driver/mysql" "gorm.io/gorm" "net/http" "net/http/httptest" "testing" ) type header struct { Key string Value string } func performRequest(r http.Handler, method, path string, headers ...header) *httptest.ResponseRecorder { req := httptest.NewRequest(method, path, nil) for _, h := range headers { req.Header.Add(h.Key, h.Value) } w := httptest.NewRecorder() r.ServeHTTP(w, req) return w } func init() { // 初始化db db, err := gorm.Open(mysql.Open(fmt.Sprintf("root:root1234@tcp(localhost:%v)/learnk2?charset=utf8&parseTime=True&loc=Local", 31234)), &gorm.Config{}) if err != nil { panic(fmt.Sprintf("failed to connect db, got error: %v, port: %v", err, 31234)) } Runtime.db = db // 初始化 casbin casbin, err := Setup(db) if err != nil { panic(fmt.Sprintf("failed to init casbin, got error: %v", err)) } Runtime.casbin = casbin } // 1,2,3,4,5就当是token了 var headers = []header{ { Key: "Authentication", Value: "1", }, { Key: "Authentication", Value: "2", }, { Key: "Authentication", Value: "3", }, { Key: "Authentication", Value: "4", }, { Key: "Authentication", Value: "5", }, { Key: "Authentication", Value: "6", }, } func TestAuth(t *testing.T) { router := gin.New() router.Use(AuthCheckRole()) router.GET("/api/v1/test", func(c *gin.Context) { fmt.Println("hello") }) //api资源为 /api/v1/test //做不同角色的case测试 for _,h := range headers { w := performRequest(router, "GET", "/api/v1/test",h) fmt.Println(w.Body.String()) } }
-
-
手动配置数据库资源
-
当角色为业务管理员businessAdmin,放过
-
当角色为正式成员formalMember, 放过
-
数据库casbin_rule表
id ptype v0 v1 v2 v3 v4 v5 1 p businessAdmin /api/v1/test GET 1 p formalMember /api/v1/test GET
-
测试结果
-
测试预期
- [zhangsan] 为 [admin] 自动放过。 打印hello
- wangwu 为 [businessAdmin(业务管理员)] 可通过。 打印hello
- lisi 为 [formalMember(正式成员)] 可通过。 打印hello
- zhaoliu 为 [normalUser(普通成员)] 不可通过,打印无权限访问。
- tianqi 为 [formalMember(正式成员)] 可通过。 打印hello
- token=6 为无权限访问,打印401
-
执行结果
=== RUN TestAuth [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode) [GIN-debug] GET /api/v1/test --> learnk2/common/casbin.TestAuth.func1 (2 handlers) 用户:zhangsan, 是 admin, 直接通过! hello username :lisi, isTrue: true, role: formalMember method: GET path: /api/v1/test hello username :wangwu, isTrue: true, role: businessAdmin method: GET path: /api/v1/test hello username :zhaoliu,isTrue: false, role: normalUser method: GET path: /api/v1/test message: 当前request无权限,请管理员确认! {"code":403,"msg":"对不起,您没有该接口访问权限,请联系管理员"} username :tianqi, isTrue: true, role: formalMember method: GET path: /api/v1/test hello {"code":401,"msg":"token过期,不存在"} --- PASS: TestAuth (0.00s) PASS
-
结论
- 整体流程下来符合预期,将其封装成中间件,拿来即用效果更好。
五、总结
- casbin权限这一块做的挺全面,覆盖的权限模型基本上满足日常开发使用,包括RBAC,ABAC,ACL,Restful等模型。简单学习即可上手开发。
- 熟练掌握各种模式,和casbin的api使用,在项目中可以解决权限的大部分问题。