Gin集成Casbin进行访问权限控制

时间:2022-09-24 18:51:50

Gin集成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 文件可以包含注释。注释以 # 开头, # 会注释该行剩余部分。

比如:

  1. # Request定义 
  2. [request_definition] 
  3. r = sub, obj, act 
  4.  
  5. # 策略定义 
  6. [policy_definition] 
  7. p = sub, obj, act 
  8.  
  9. # 角色定义 
  10. [role_definition] 
  11. g = _, _ 
  12.  
  13. [policy_effect] 
  14. e = some(where (p.eft == allow)) 
  15.  
  16. # 匹配器定义 
  17. [matchers] 
  18. 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主要表示访问控制关于角色、资源、行为的具体映射关系。

比如:

  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 AlterAlter Routine, CreateCreate Routine, Create Temporary Tables, Create ViewDeleteDrop, Event, ExecuteIndexInsert, Lock Tables, ReferencesSelect, Show ViewTriggerUpdate 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仓库[3],可以自行阅读,这些仅仅贴部分关键代码。

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

  1. [request_definition] 
  2. r = sub, obj, act 
  3.  
  4. [policy_definition] 
  5. p = sub, obj, act 
  6.  
  7. [role_definition] 
  8. g = _, _ 
  9.  
  10. [policy_effect] 
  11. e = some(where (p.eft == allow)) 
  12.  
  13. [matchers] 
  14. 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.  
  42.  adapter := gormadapter.NewAdapterByDB(db) 
  43.  enforcer := casbin.NewEnforcer(global.CasbinSetting.ModelPath, adapter) 
  44.  enforcer.AddFunction("ParamsMatch", ParamsMatchFunc) 
  45.  _ = enforcer.LoadPolicy() 
  46.  return enforcer 
  47.  
  48. //@function: ParamsMatch 
  49. //@description: 自定义规则函数 
  50. //@param: fullNameKey1 string, key2 string 
  51. //@return: bool 
  52. func ParamsMatch(fullNameKey1 string, key2 string) bool { 
  53.  key1 := strings.Split(fullNameKey1, "?")[0] 
  54.  // 剥离路径后再使用casbin的keyMatch2 
  55.  return util.KeyMatch2(key1, key2) 
  56.  
  57. //@function: ParamsMatchFunc 
  58. //@description: 自定义规则函数 
  59. //@param: args ...interface{} 
  60. //@return: interface{}, error 
  61. func ParamsMatchFunc(args ...interface{}) (interface{}, error) { 
  62.  name1 := args[0].(string) 
  63.  name2 := args[1].(string) 
  64.  
  65.  return ParamsMatch(name1, name2), nil 

(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) 

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

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

(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.  // 进行插入操作 
  29.  svc := service.NewService(ctx) 
  30.  err := svc.CasbinCreate(&param) 
  31.  if err != nil { 
  32.   log.Printf("svc.CasbinCreate err: %v", err) 
  33.   response.ToErrorResponse(errcode.ErrorCasbinCreateFail) 
  34.  } 
  35.  response.ToResponse(gin.H{}) 
  36.  return 
  37.  
  38.  
  39. // List godoc 
  40. // @Summary 获取权限列表 
  41. // @Produce json 
  42. // @Tags 权限管理 
  43. // @Security ApiKeyAuth 
  44. // @Param data body service.CasbinListRequest true "角色ID" 
  45. // @Success 200 {object} service.CasbinListResponse "成功" 
  46. // @Failure 400 {object} errcode.Error "请求错误" 
  47. // @Failure 500 {object} errcode.Error "内部错误" 
  48. // @Router /api/v1/casbin/list [post] 
  49. func (c Casbin) List(ctx *gin.Context) { 
  50.  param := service.CasbinListRequest{} 
  51.  response := app.NewResponse(ctx) 
  52.  valid, errors := app.BindAndValid(ctx, &param) 
  53.  if !valid { 
  54.   log.Printf("app.BindAndValid errs: %v", errors) 
  55.   errRsp := errcode.InvalidParams.WithDetails(errors.Errors()...) 
  56.   response.ToErrorResponse(errRsp) 
  57.   return 
  58.  } 
  59.  // 业务逻辑处理 
  60.  svc := service.NewService(ctx) 
  61.  casbins := svc.CasbinList(&param) 
  62.  var respList []service.CasbinInfo 
  63.  for _, host := range casbins { 
  64.   respList = append(respList, service.CasbinInfo{ 
  65.    Path:   host[1], 
  66.    Method: host[2], 
  67.   }) 
  68.  } 
  69.  response.ToResponseList(respList, 0) 
  70.  return 

再在该目录下创建一个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请求成功"

(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.  } 

(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.   // 权限策略管理 
  14.   apiv1.POST("/casbin", casbin.Create
  15.   apiv1.POST("/casbin/list", casbin.List) 
  16.  } 
  17.  return r 

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

验证

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

Gin集成Casbin进行访问权限控制

(2)将测试路径添加到权限列表,如下:

Gin集成Casbin进行访问权限控制

(3)然后再次访问测试路径,如下:

Gin集成Casbin进行访问权限控制

并且从日志上也可以看到,如下:

Gin集成Casbin进行访问权限控制

参考文档:

[1] https://casbin.org/

[2] https://casbin.org/docs/zh-CN/overview

[3] https://gitee.com/coolops/casbin_test.git

原文地址:https://mp.weixin.qq.com/s/Igi_xWO-XqEIuHOwabSmMg