Casbin是什么

Casbin是一个强大的、高效的开源访问控制框架,其权限管理机制支持多种访问控制模型,Casbin只负责访问控制。

其功能有:

  • 支持自定义请求的格式,默认的请求格式为{subject, object, action}
  • 具有访问控制模型model和策略policy两个核心概念。
  • 支持RBAC中的多层角色继承,不止主体可以有角色,资源也可以具有角色。
  • 支持内置的超级用户 例如:rootadministrator。超级用户可以执行任何操作而无需显式的权限声明。
  • 支持多种内置的操作符,如 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 文件可以包含注释。注释以 # 开头, # 会注释该行剩余部分。

比如:

  1. # Request定义
  2. [request_definition]
  3. r = sub, obj, act
  4. # 策略定义
  5. [policy_definition]
  6. p = sub, obj, act
  7. # 角色定义
  8. [role_definition]
  9. g = _, _
  10. [policy_effect]
  11. e = some(where (p.eft == allow))
  12. # 匹配器定义
  13. [matchers]
  14. 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的定义,每条规则通常以形如ppolicy type开头,比如p,joker,data1,read就是一条joker具有data1读权限的规则。
  • roledefinition:是RBAC角色继承关系的定义。g 是一个 RBAC系统,`, _`表示角色继承关系的前项和后项,即前项继承后项角色的权限。
  • policy_effect:是对policy生效范围的定义,它对request的决策结果进行统一的决策,比如e = some(where (p.eft == allow))就表示如果存在任意一个决策结果为allow的匹配规则,则最终决策结果为allowp.eft 表示策略规则的决策结果,可以为allow 或者deny,当不指定规则的决策结果时,取默认值allow
  • matchers:定义了策略匹配者。匹配者是一组表达式,它定义了如何根据请求来匹配策略规则

Policy

Policy主要表示访问控制关于角色、资源、行为的具体映射关系。

比如:

  1. p, alice, data1, read
  2. p, bob, data2, write
  3. p, data2_admin, data2, read
  4. p, data2_admin, data2, write
  5. g, alice, data2_admin

它的关系规则很简单,主要是选择什么方式来存储规则,目前官方提供csv文件存储和通过adapter适配器从其他存储系统中加载配置文件,比如MySQL, PostgreSQL, SQL Server, SQLite3,MongoDB,Redis,Cassandra DB等。

实践

创建项目

首先创建一个项目,叫casbin_test。

项目里的目录结构如下:

  1. ├─configs # 配置文件
  2. ├─global # 全局变量
  3. ├─internal # 内部模块
  4. ├─dao # 数据处理模块
  5. ├─middleware # 中间件
  6. ├─model # 模型层
  7. ├─router # 路由
  8. └─api
  9. └─v1 # 视图
  10. └─service # 业务逻辑层
  11. └─pkg # 内部模块包
  12. ├─app # 应用包
  13. ├─errcode # 错误代码包
  14. └─setting # 配置包

下载依赖包,如下:

  1. go get -u github.com/gin-gonic/gin
  2. # Go语言casbin的依赖包
  3. go get github.com/casbin/casbin
  4. # gorm 适配器依赖包
  5. go get github.com/casbin/gorm-adapter
  6. # mysql驱动依赖
  7. go get github.com/go-sql-driver/mysql
  8. # gorm 包
  9. go get github.com/jinzhu/gorm

创建数据库,如下:

  1. CREATE DATABASE `casbin_test` DEFAULT CHARACTER SET utf8;
  2. GRANT Alter, Alter Routine, Create, Create Routine, Create Temporary Tables, Create View, Delete, Drop, Event, Execute, Index, Insert, Lock Tables, References, Select, Show View, Trigger, Update ON `casbin\_test`.* TO `ops`@`%`;
  3. FLUSH PRIVILEGES;
  4. DROP TABLE IF EXIST `casbin_rule`;
  5. CREATE TABLE `casbin_rule` (
  6. `p_type` varchar(100) DEFAULT NULL COMMENT '规则类型',
  7. `v0` varchar(100) DEFAULT NULL COMMENT '角色ID',
  8. `v1` varchar(100) DEFAULT NULL COMMENT 'api路径',
  9. `v2` varchar(100) DEFAULT NULL COMMENT 'api访问方法',
  10. `v3` varchar(100) DEFAULT NULL,
  11. `v4` varchar(100) DEFAULT NULL,
  12. `v5` varchar(100) DEFAULT NULL
  13. ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='权限规则表';
  14. /*插入操作casbin api的权限规则*/
  15. INSERT INTO `casbin_rule`(`p_type`, `v0`, `v1`, `v2`) VALUES ('p', 'admin', '/api/v1/casbin', 'POST');
  16. INSERT INTO `casbin_rule`(`p_type`, `v0`, `v1`, `v2`) VALUES ('p', 'admin', '/api/v1/casbin/list', 'GET');

代码开发

由于代码比较多,这里就不贴全部代码了,全部代码已经放在gitee仓库,可以自行阅读,这些仅仅贴部分关键代码。

(1)首先在configs目录下创建rbac_model.conf文件,写入如下代码:

  1. [request_definition]
  2. r = sub, obj, act
  3. [policy_definition]
  4. p = sub, obj, act
  5. [role_definition]
  6. g = _, _
  7. [policy_effect]
  8. e = some(where (p.eft == allow))
  9. [matchers]
  10. m = r.sub == p.sub && ParamsMatch(r.obj,p.obj) && r.act == p.act

(2)在internal/model目录下,创建casbin.go文件,写入如下代码:

  1. type CasbinModel struct {
  2. PType string `json:"p_type" gorm:"column:p_type" description:"策略类型"`
  3. RoleId string `json:"role_id" gorm:"column:v0" description:"角色ID"`
  4. Path string `json:"path" gorm:"column:v1" description:"api路径"`
  5. Method string `json:"method" gorm:"column:v2" description:"访问方法"`
  6. }
  7. func (c *CasbinModel) TableName() string {
  8. return "casbin_rule"
  9. }
  10. func (c *CasbinModel) Create(db *gorm.DB) error {
  11. e := Casbin()
  12. if success := e.AddPolicy(c.RoleId,c.Path,c.Method); success == false {
  13. return errors.New("存在相同的API,添加失败")
  14. }
  15. return nil
  16. }
  17. func (c *CasbinModel) Update(db *gorm.DB, values interface{}) error {
  18. if err := db.Model(c).Where("v1 = ? AND v2 = ?", c.Path, c.Method).Update(values).Error; err != nil {
  19. return err
  20. }
  21. return nil
  22. }
  23. func (c *CasbinModel) List(db *gorm.DB) [][]string {
  24. e := Casbin()
  25. policy := e.GetFilteredPolicy(0, c.RoleId)
  26. return policy
  27. }
  28. //@function: Casbin
  29. //@description: 持久化到数据库 引入自定义规则
  30. //@return: *casbin.Enforcer
  31. func Casbin() *casbin.Enforcer {
  32. s := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=%s&parseTime=%t&loc=Local",
  33. global.DatabaseSetting.Username,
  34. global.DatabaseSetting.Password,
  35. global.DatabaseSetting.Host,
  36. global.DatabaseSetting.DBName,
  37. global.DatabaseSetting.Charset,
  38. global.DatabaseSetting.ParseTime,
  39. )
  40. db, _ := gorm.Open(global.DatabaseSetting.DBType, s)
  41. adapter := gormadapter.NewAdapterByDB(db)
  42. enforcer := casbin.NewEnforcer(global.CasbinSetting.ModelPath, adapter)
  43. enforcer.AddFunction("ParamsMatch", ParamsMatchFunc)
  44. _ = enforcer.LoadPolicy()
  45. return enforcer
  46. }
  47. //@function: ParamsMatch
  48. //@description: 自定义规则函数
  49. //@param: fullNameKey1 string, key2 string
  50. //@return: bool
  51. func ParamsMatch(fullNameKey1 string, key2 string) bool {
  52. key1 := strings.Split(fullNameKey1, "?")[0]
  53. // 剥离路径后再使用casbin的keyMatch2
  54. return util.KeyMatch2(key1, key2)
  55. }
  56. //@function: ParamsMatchFunc
  57. //@description: 自定义规则函数
  58. //@param: args ...interface{}
  59. //@return: interface{}, error
  60. func ParamsMatchFunc(args ...interface{}) (interface{}, error) {
  61. name1 := args[0].(string)
  62. name2 := args[1].(string)
  63. return ParamsMatch(name1, name2), nil
  64. }

(3)在internal/dao目录下创建casbin.go,写入如下代码:

  1. func (d *Dao) CasbinCreate(roleId string, path, method string) error {
  2. cm := model.CasbinModel{
  3. PType: "p",
  4. RoleId: roleId,
  5. Path: path,
  6. Method: method,
  7. }
  8. return cm.Create(d.engine)
  9. }
  10. func (d *Dao) CasbinList(roleID string) [][]string {
  11. cm := model.CasbinModel{RoleId: roleID}
  12. return cm.List(d.engine)
  13. }

(4)在internal/service目录下创建service.go,写入如下代码:

  1. type CasbinInfo struct {
  2. Path string `json:"path" form:"path"`
  3. Method string `json:"method" form:"method"`
  4. }
  5. type CasbinCreateRequest struct {
  6. RoleId string `json:"role_id" form:"role_id" description:"角色ID"`
  7. CasbinInfos []CasbinInfo `json:"casbin_infos" description:"权限模型列表"`
  8. }
  9. type CasbinListResponse struct {
  10. List []CasbinInfo `json:"list" form:"list"`
  11. }
  12. type CasbinListRequest struct {
  13. RoleID string `json:"role_id" form:"role_id"`
  14. }
  15. func (s Service) CasbinCreate(param *CasbinCreateRequest) error {
  16. for _, v := range param.CasbinInfos {
  17. err := s.dao.CasbinCreate(param.RoleId, v.Path, v.Method)
  18. if err != nil {
  19. return err
  20. }
  21. }
  22. return nil
  23. }
  24. func (s Service) CasbinList(param *CasbinListRequest) [][]string {
  25. return s.dao.CasbinList(param.RoleID)
  26. }

(5)在internal/router/api/v1目录下创建casbin.go,写入如下代码:

  1. type Casbin struct {
  2. }
  3. func NewCasbin() Casbin {
  4. return Casbin{}
  5. }
  6. // Create godoc
  7. // @Summary 新增权限
  8. // @Description 新增权限
  9. // @Tags 权限管理
  10. // @Produce json
  11. // @Security ApiKeyAuth
  12. // @Param body body service.CasbinCreateRequest true "body"
  13. // @Success 200 {object} string "成功"
  14. // @Failure 400 {object} errcode.Error "请求错误"
  15. // @Failure 500 {object} errcode.Error "内部错误"
  16. // @Router /api/v1/casbin [post]
  17. func (c Casbin) Create(ctx *gin.Context) {
  18. param := service.CasbinCreateRequest{}
  19. response := app.NewResponse(ctx)
  20. valid, errors := app.BindAndValid(ctx, &param)
  21. if !valid {
  22. log.Printf("app.BindAndValid errs: %v", errors)
  23. errRsp := errcode.InvalidParams.WithDetails(errors.Errors()...)
  24. response.ToErrorResponse(errRsp)
  25. return
  26. }
  27. // 进行插入操作
  28. svc := service.NewService(ctx)
  29. err := svc.CasbinCreate(&param)
  30. if err != nil {
  31. log.Printf("svc.CasbinCreate err: %v", err)
  32. response.ToErrorResponse(errcode.ErrorCasbinCreateFail)
  33. }
  34. response.ToResponse(gin.H{})
  35. return
  36. }
  37. // List godoc
  38. // @Summary 获取权限列表
  39. // @Produce json
  40. // @Tags 权限管理
  41. // @Security ApiKeyAuth
  42. // @Param data body service.CasbinListRequest true "角色ID"
  43. // @Success 200 {object} service.CasbinListResponse "成功"
  44. // @Failure 400 {object} errcode.Error "请求错误"
  45. // @Failure 500 {object} errcode.Error "内部错误"
  46. // @Router /api/v1/casbin/list [post]
  47. func (c Casbin) List(ctx *gin.Context) {
  48. param := service.CasbinListRequest{}
  49. response := app.NewResponse(ctx)
  50. valid, errors := app.BindAndValid(ctx, &param)
  51. if !valid {
  52. log.Printf("app.BindAndValid errs: %v", errors)
  53. errRsp := errcode.InvalidParams.WithDetails(errors.Errors()...)
  54. response.ToErrorResponse(errRsp)
  55. return
  56. }
  57. // 业务逻辑处理
  58. svc := service.NewService(ctx)
  59. casbins := svc.CasbinList(&param)
  60. var respList []service.CasbinInfo
  61. for _, host := range casbins {
  62. respList = append(respList, service.CasbinInfo{
  63. Path: host[1],
  64. Method: host[2],
  65. })
  66. }
  67. response.ToResponseList(respList, 0)
  68. return
  69. }

再在该目录下创建一个test.go文件,用于测试,代码如下:

  1. type Test struct {
  2. }
  3. func NewTest() Test {
  4. return Test{}
  5. }
  6. func (t Test) Get(ctx *gin.Context) {
  7. log.Println("Hello 接收到GET请求..")
  8. response := app.NewResponse(ctx)
  9. response.ToResponse("接收GET请求成功")
  10. }

(6)在internal/middleware目录下创建casbin_handler.go,写入如下代码:

  1. func CasbinHandler() gin.HandlerFunc {
  2. return func(ctx *gin.Context) {
  3. response := app.NewResponse(ctx)
  4. // 获取请求的URI
  5. obj := ctx.Request.URL.RequestURI()
  6. // 获取请求方法
  7. act := ctx.Request.Method
  8. // 获取用户的角色
  9. sub := "admin"
  10. e := model.Casbin()
  11. fmt.Println(obj, act, sub)
  12. // 判断策略中是否存在
  13. success := e.Enforce(sub, obj, act)
  14. if success {
  15. log.Println("恭喜您,权限验证通过")
  16. ctx.Next()
  17. } else {
  18. log.Printf("e.Enforce err: %s", "很遗憾,权限验证没有通过")
  19. response.ToErrorResponse(errcode.UnauthorizedAuthFail)
  20. ctx.Abort()
  21. return
  22. }
  23. }
  24. }

(7)在internal/router目录下创建router.go,定义路由,代码如下:

  1. func NewRouter() *gin.Engine {
  2. r := gin.New()
  3. r.Use(gin.Logger())
  4. r.Use(gin.Recovery())
  5. casbin := v1.NewCasbin()
  6. test := v1.NewTest()
  7. apiv1 := r.Group("/api/v1")
  8. apiv1.Use(middleware.CasbinHandler())
  9. {
  10. // 测试路由
  11. apiv1.GET("/hello", test.Get)
  12. // 权限策略管理
  13. apiv1.POST("/casbin", casbin.Create)
  14. apiv1.POST("/casbin/list", casbin.List)
  15. }
  16. return r
  17. }

最后就启动项目进行测试。

验证

(1)首先访问测试路径,当前情况下没在权限表里,如下:
image.png

(2)将测试路径添加到权限列表,如下:
image.png
(3)然后再次访问测试路径,如下:
image.png
并且从日志上也可以看到,如下:
image.png

参考文档:

[1] https://casbin.org/
[2] https://casbin.org/docs/zh-CN/overview
[3] https://gitee.com/coolops/casbin_test.git