Casbin是一个强大的、高效的开源访问控制框架,其权限管理机制支持多种访问控制模型,Casbin只负责访问控制.
Casbin是什么
Casbin是一个强大的、高效的开源访问控制框架,其权限管理机制支持多种访问控制模型,Casbin只负责访问控制[1]。
其功能有:
- 支持自定义请求的格式,默认的请求格式为{subject, object, action}。.
- 具有访问控制模型model和策略policy两个核心概念。
- 支持RBAC中的多层角色继承,不止主体可以有角色,资源也可以具有角色。
- 支持内置的超级用户 例如:root或administrator。超级用户可以执行任何操作而无需显式的权限声明。
- 支持多种内置的操作符,如 keyMatch,方便对路径式的资源进行管理,如 /foo/bar可以映射到 /foo*
Casbin的工作原理
在 Casbin 中, 访问控制模型被抽象为基于 **PERM **(Policy, Effect, Request, Matcher) [策略,效果,请求,匹配器]的一个文件。
- Policy:定义权限的规则
- Effect:定义组合了多个Policy之后的结果
- Request:访问请求
- Matcher:判断Request是否满足Policy
首先会定义一堆Policy,然后通过Matcher来判断Request和Policy是否匹配,然后通过Effect来判断匹配结果是Allow还是Deny。
Casbin的核心概念
Model
Model是Casbin的具体访问模型,其主要以文件的形式出现,该文件常常以.conf最为后缀。
- Model CONF 至少应包含四个部分: [request_definition], [policy_definition], [policy_effect], [matchers]。
- 如果 model 使用 RBAC, 还需要添加[role_definition]部分。
- Model CONF 文件可以包含注释。注释以 # 开头, # 会注释该行剩余部分。
比如:
- #Request定义
- [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)&&r.obj==p.obj&&r.act==p.act
- request_definition:用于request的定义,它明确了e.Enforce(...)函数中参数的定义,sub, obj, act 表示经典三元组: 访问实体 (Subject),访问资源 (Object) 和访问方法 (Action)。
- policy_definition:用于policy的定义,每条规则通常以形如p的policy type开头,比如p,joker,data1,read就是一条joker具有data1读权限的规则。
- role_definition:是RBAC角色继承关系的定义。g 是一个 RBAC系统,_, _表示角色继承关系的前项和后项,即前项继承后项角色的权限。
- policy_effect:是对policy生效范围的定义,它对request的决策结果进行统一的决策,比如e = some(where (p.eft == allow))就表示如果存在任意一个决策结果为allow的匹配规则,则最终决策结果为allow。p.eft 表示策略规则的决策结果,可以为allow 或者deny,当不指定规则的决策结果时,取默认值allow 。
-
matchers:定义了策略匹配者。匹配者是一组表达式,它定义了如何根据请求来匹配策略规则
Policy
Policy主要表示访问控制关于角色、资源、行为的具体映射关系。
比如:
- p,alice,data1,read
- p,bob,data2,write
- p,data2_admin,data2,read
- p,data2_admin,data2,write
- g,alice,data2_admin
它的关系规则很简单,主要是选择什么方式来存储规则,目前官方提供csv文件存储和通过adapter适配器从其他存储系统中加载配置文件,比如MySQL, PostgreSQL, SQL Server, SQLite3,MongoDB,Redis,Cassandra DB等。
实践
创建项目
首先创建一个项目,叫Casbin_test。
项目里的目录结构如下:
- ├─configs#配置文件
- ├─global#全局变量
- ├─internal#内部模块
- │├─dao#数据处理模块
- │├─middleware#中间件
- │├─model#模型层
- │├─router#路由
- ││└─api
- ││└─v1#视图
- │└─service#业务逻辑层
- └─pkg#内部模块包
- ├─app#应用包
- ├─errcode#错误代码包
- └─setting#配置包
下载依赖包,如下:
- goget-ugithub.com/gin-gonic/gin
- #Go语言casbin的依赖包
- gogetgithub.com/casbin/casbin
- #gorm适配器依赖包
- gogetgithub.com/casbin/gorm-adapter
- #mysql驱动依赖
- gogetgithub.com/go-sql-driver/mysql
- #gorm包
- gogetgithub.com/jinzhu/gorm
创建数据库,如下:
- CREATEDATABASE`casbin_test`DEFAULTCHARACTERSETutf8;
- GRANTAlter,AlterRoutine,Create,CreateRoutine,CreateTemporaryTables,CreateView,Delete,Drop,Event,Execute,Index,Insert,LockTables,References,Select,ShowView,Trigger,UpdateON`casbin\_test`.*TO`ops`@`%`;
- FLUSHPRIVILEGES;
- DROPTABLEIFEXIST`casbin_rule`;
- CREATETABLE`casbin_rule`(
- `p_type`varchar(100)DEFAULTNULLCOMMENT'规则类型',
- `v0`varchar(100)DEFAULTNULLCOMMENT'角色ID',
- `v1`varchar(100)DEFAULTNULLCOMMENT'api路径',
- `v2`varchar(100)DEFAULTNULLCOMMENT'api访问方法',
- `v3`varchar(100)DEFAULTNULL,
- `v4`varchar(100)DEFAULTNULL,
- `v5`varchar(100)DEFAULTNULL
- )ENGINE=InnoDBDEFAULTCHARSET=utf8COMMENT='权限规则表';
- /*插入操作casbinapi的权限规则*/
- INSERTINTO`casbin_rule`(`p_type`,`v0`,`v1`,`v2`)VALUES('p','admin','/api/v1/casbin','POST');
- INSERTINTO`casbin_rule`(`p_type`,`v0`,`v1`,`v2`)VALUES('p','admin','/api/v1/casbin/list','GET');
代码开发
由于代码比较多,这里就不贴全部代码了,全部代码已经放在gitee仓库[3],可以自行阅读,这些仅仅贴部分关键代码。
(1)首先在configs目录下创建rbac_model.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=r.sub==p.sub&&ParamsMatch(r.obj,p.obj)&&r.act==p.act
(2)在internal/model目录下,创建casbin.go文件,写入如下代码:
- typeCasbinModelstruct{
- PTypestring`json:"p_type"gorm:"column:p_type"description:"策略类型"`
- RoleIdstring`json:"role_id"gorm:"column:v0"description:"角色ID"`
- Pathstring`json:"path"gorm:"column:v1"description:"api路径"`
- Methodstring`json:"method"gorm:"column:v2"description:"访问方法"`
- }
- func(c*CasbinModel)TableName()string{
- return"casbin_rule"
- }
- func(c*CasbinModel)Create(db*gorm.DB)error{
- e:=Casbin()
- ifsuccess:=e.AddPolicy(c.RoleId,c.Path,c.Method);success==false{
- returnerrors.New("存在相同的API,添加失败")
- }
- returnnil
- }
- func(c*CasbinModel)Update(db*gorm.DB,valuesinterface{})error{
- iferr:=db.Model(c).Where("v1=?ANDv2=?",c.Path,c.Method).Update(values).Error;err!=nil{
- returnerr
- }
- returnnil
- }
- func(c*CasbinModel)List(db*gorm.DB)[][]string{
- e:=Casbin()
- policy:=e.GetFilteredPolicy(0,c.RoleId)
- returnpolicy
- }
- //@function:Casbin
- //@description:持久化到数据库引入自定义规则
- //@return:*casbin.Enforcer
- funcCasbin()*casbin.Enforcer{
- s:=fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=%s&parseTime=%t&loc=Local",
- global.DatabaseSetting.Username,
- global.DatabaseSetting.Password,
- global.DatabaseSetting.Host,
- global.DatabaseSetting.DBName,
- global.DatabaseSetting.Charset,
- global.DatabaseSetting.ParseTime,
- )
- db,_:=gorm.Open(global.DatabaseSetting.DBType,s)
- adapter:=gormadapter.NewAdapterByDB(db)
- enforcer:=casbin.NewEnforcer(global.CasbinSetting.ModelPath,adapter)
- enforcer.AddFunction("ParamsMatch",ParamsMatchFunc)
- _=enforcer.LoadPolicy()
- returnenforcer
- }
- //@function:ParamsMatch
- //@description:自定义规则函数
- //@param:fullNameKey1string,key2string
- //@return:bool
- funcParamsMatch(fullNameKey1string,key2string)bool{
- key1:=strings.Split(fullNameKey1,"?")[0]
- //剥离路径后再使用casbin的keyMatch2
- returnutil.KeyMatch2(key1,key2)
- }
- //@function:ParamsMatchFunc
- //@description:自定义规则函数
- //@param:args...interface{}
- //@return:interface{},error
- funcParamsMatchFunc(args...interface{})(interface{},error){
- name1:=args[0].(string)
- name2:=args[1].(string)
- returnParamsMatch(name1,name2),nil
- }
(3)在internal/dao目录下创建casbin.go,写入如下代码:
- func(d*Dao)CasbinCreate(roleIdstring,path,methodstring)error{
- cm:=model.CasbinModel{
- PType:"p",
- RoleId:roleId,
- Path:path,
- Method:method,
- }
- returncm.Create(d.engine)
- }
- func(d*Dao)CasbinList(roleIDstring)[][]string{
- cm:=model.CasbinModel{RoleId:roleID}
- returncm.List(d.engine)
- }
(4)在internal/service目录下创建service.go,写入如下代码:
- typeCasbinInfostruct{
- Pathstring`json:"path"form:"path"`
- Methodstring`json:"method"form:"method"`
- }
- typeCasbinCreateRequeststruct{
- RoleIdstring`json:"role_id"form:"role_id"description:"角色ID"`
- CasbinInfos[]CasbinInfo`json:"casbin_infos"description:"权限模型列表"`
- }
- typeCasbinListResponsestruct{
- List[]CasbinInfo`json:"list"form:"list"`
- }
- typeCasbinListRequeststruct{
- RoleIDstring`json:"role_id"form:"role_id"`
- }
- func(sService)CasbinCreate(param*CasbinCreateRequest)error{
- for_,v:=rangeparam.CasbinInfos{
- err:=s.dao.CasbinCreate(param.RoleId,v.Path,v.Method)
- iferr!=nil{
- returnerr
- }
- }
- returnnil
- }
- func(sService)CasbinList(param*CasbinListRequest)[][]string{
- returns.dao.CasbinList(param.RoleID)
- }
(5)在internal/router/api/v1目录下创建casbin.go,写入如下代码:
- typeCasbinstruct{
- }
- funcNewCasbin()Casbin{
- returnCasbin{}
- }
- //Creategodoc
- //@Summary新增权限
- //@Description新增权限
- //@Tags权限管理
- //@Producejson
- //@SecurityApiKeyAuth
- //@Parambodybodyservice.CasbinCreateRequesttrue"body"
- //@Success200{object}string"成功"
- //@Failure400{object}errcode.Error"请求错误"
- //@Failure500{object}errcode.Error"内部错误"
- //@Router/api/v1/casbin[post]
- func(cCasbin)Create(ctx*gin.Context){
- param:=service.CasbinCreateRequest{}
- response:=app.NewResponse(ctx)
- valid,errors:=app.BindAndValid(ctx,¶m)
- if!valid{
- log.Printf("app.BindAndValiderrs:%v",errors)
- errRsp:=errcode.InvalidParams.WithDetails(errors.Errors()...)
- response.ToErrorResponse(errRsp)
- return
- }
- //进行插入操作
- svc:=service.NewService(ctx)
- err:=svc.CasbinCreate(¶m)
- iferr!=nil{
- log.Printf("svc.CasbinCreateerr:%v",err)
- response.ToErrorResponse(errcode.ErrorCasbinCreateFail)
- }
- response.ToResponse(gin.H{})
- return
- }
- //Listgodoc
- //@Summary获取权限列表
- //@Producejson
- //@Tags权限管理
- //@SecurityApiKeyAuth
- //@Paramdatabodyservice.CasbinListRequesttrue"角色ID"
- //@Success200{object}service.CasbinListResponse"成功"
- //@Failure400{object}errcode.Error"请求错误"
- //@Failure500{object}errcode.Error"内部错误"
- //@Router/api/v1/casbin/list[post]
- func(cCasbin)List(ctx*gin.Context){
- param:=service.CasbinListRequest{}
- response:=app.NewResponse(ctx)
- valid,errors:=app.BindAndValid(ctx,¶m)
- if!valid{
- log.Printf("app.BindAndValiderrs:%v",errors)
- errRsp:=errcode.InvalidParams.WithDetails(errors.Errors()...)
- response.ToErrorResponse(errRsp)
- return
- }
- //业务逻辑处理
- svc:=service.NewService(ctx)
- casbins:=svc.CasbinList(¶m)
- varrespList[]service.CasbinInfo
- for_,host:=rangecasbins{
- respList=append(respList,service.CasbinInfo{
- Path:host[1],
- Method:host[2],
- })
- }
- response.ToResponseList(respList,0)
- return
- }
再在该目录下创建一个test.go文件,用于测试,代码如下:
0
- p,alice,data1,read
- p,bob,data2,write
- p,data2_admin,data2,read
- p,data2_admin,data2,write
- g,alice,data2_admin
(6)在internal/middleware目录下创建casbin_handler.go,写入如下代码:
1
- p,alice,data1,read
- p,bob,data2,write
- p,data2_admin,data2,read
- p,data2_admin,data2,write
- g,alice,data2_admin
(7)在internal/router目录下创建router.go,定义路由,代码如下:
2
- p,alice,data1,read
- p,bob,data2,write
- p,data2_admin,data2,read
- p,data2_admin,data2,write
- g,alice,data2_admin
最后就启动项目进行测试。
验证
(1)首先访问测试路径,当前情况下没在权限表里,如下:
(2)将测试路径添加到权限列表,如下:
(3)然后再次访问测试路径,如下:
并且从日志上也可以看到,如下:
参考文档:
[1] https://casbin.org/
[2] https://casbin.org/docs/zh-CN/overview
[3] https://gitee.com/coolops/casbin_test.git
知优网 » Gin集成Casbin进行访问权限控制