Gin集成Casbin進(jìn)行訪問權(quán)限控制
Casbin是什么
Casbin是一個(gè)強(qiáng)大的、高效的開源訪問控制框架,其權(quán)限管理機(jī)制支持多種訪問控制模型,Casbin只負(fù)責(zé)訪問控制[1]。
其功能有:
- 支持自定義請(qǐng)求的格式,默認(rèn)的請(qǐng)求格式為{subject, object, action}。.
 - 具有訪問控制模型model和策略policy兩個(gè)核心概念。
 - 支持RBAC中的多層角色繼承,不止主體可以有角色,資源也可以具有角色。
 - 支持內(nèi)置的超級(jí)用戶 例如:root或administrator。超級(jí)用戶可以執(zhí)行任何操作而無需顯式的權(quán)限聲明。
 - 支持多種內(nèi)置的操作符,如 keyMatch,方便對(duì)路徑式的資源進(jìn)行管理,如 /foo/bar可以映射到 /foo*
 
Casbin的工作原理
在 Casbin 中, 訪問控制模型被抽象為基于 **PERM **(Policy, Effect, Request, Matcher) [策略,效果,請(qǐng)求,匹配器]的一個(gè)文件。
- Policy:定義權(quán)限的規(guī)則
 - Effect:定義組合了多個(gè)Policy之后的結(jié)果
 - Request:訪問請(qǐng)求
 - Matcher:判斷Request是否滿足Policy
 
首先會(huì)定義一堆Policy,然后通過Matcher來判斷Request和Policy是否匹配,然后通過Effect來判斷匹配結(jié)果是Allow還是Deny。
Casbin的核心概念
Model
Model是Casbin的具體訪問模型,其主要以文件的形式出現(xiàn),該文件常常以.conf最為后綴。
- Model CONF 至少應(yīng)包含四個(gè)部分: [request_definition], [policy_definition], [policy_effect], [matchers]。
 - 如果 model 使用 RBAC, 還需要添加[role_definition]部分。
 - Model CONF 文件可以包含注釋。注釋以 # 開頭, # 會(huì)注釋該行剩余部分。
 
比如:
- # 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(...)函數(shù)中參數(shù)的定義,sub, obj, act 表示經(jīng)典三元組: 訪問實(shí)體 (Subject),訪問資源 (Object) 和訪問方法 (Action)。
 - policy_definition:用于policy的定義,每條規(guī)則通常以形如p的policy type開頭,比如p,joker,data1,read就是一條joker具有data1讀權(quán)限的規(guī)則。
 - role_definition:是RBAC角色繼承關(guān)系的定義。g 是一個(gè) RBAC系統(tǒng),_, _表示角色繼承關(guān)系的前項(xiàng)和后項(xiàng),即前項(xiàng)繼承后項(xiàng)角色的權(quán)限。
 - policy_effect:是對(duì)policy生效范圍的定義,它對(duì)request的決策結(jié)果進(jìn)行統(tǒng)一的決策,比如e = some(where (p.eft == allow))就表示如果存在任意一個(gè)決策結(jié)果為allow的匹配規(guī)則,則最終決策結(jié)果為allow。p.eft 表示策略規(guī)則的決策結(jié)果,可以為allow 或者deny,當(dāng)不指定規(guī)則的決策結(jié)果時(shí),取默認(rèn)值allow 。
 - 
    
matchers:定義了策略匹配者。匹配者是一組表達(dá)式,它定義了如何根據(jù)請(qǐng)求來匹配策略規(guī)則
 
Policy
Policy主要表示訪問控制關(guān)于角色、資源、行為的具體映射關(guān)系。
比如:
- p, alice, data1, read
 - p, bob, data2, write
 - p, data2_admin, data2, read
 - p, data2_admin, data2, write
 - g, alice, data2_admin
 
它的關(guān)系規(guī)則很簡(jiǎn)單,主要是選擇什么方式來存儲(chǔ)規(guī)則,目前官方提供csv文件存儲(chǔ)和通過adapter適配器從其他存儲(chǔ)系統(tǒng)中加載配置文件,比如MySQL, PostgreSQL, SQL Server, SQLite3,MongoDB,Redis,Cassandra DB等。
實(shí)踐
創(chuàng)建項(xiàng)目
首先創(chuàng)建一個(gè)項(xiàng)目,叫casbin_test。
項(xiàng)目里的目錄結(jié)構(gòu)如下:
- ├─configs # 配置文件
 - ├─global # 全局變量
 - ├─internal # 內(nèi)部模塊
 - │ ├─dao # 數(shù)據(jù)處理模塊
 - │ ├─middleware # 中間件
 - │ ├─model # 模型層
 - │ ├─router # 路由
 - │ │ └─api
 - │ │ └─v1 # 視圖
 - │ └─service # 業(yè)務(wù)邏輯層
 - └─pkg # 內(nèi)部模塊包
 - ├─app # 應(yīng)用包
 - ├─errcode # 錯(cuò)誤代碼包
 - └─setting # 配置包
 
下載依賴包,如下:
- go get -u github.com/gin-gonic/gin
 - # Go語言casbin的依賴包
 - go get github.com/casbin/casbin
 - # gorm 適配器依賴包
 - go get github.com/casbin/gorm-adapter
 - # mysql驅(qū)動(dòng)依賴
 - go get github.com/go-sql-driver/mysql
 - # gorm 包
 - go get github.com/jinzhu/gorm
 
創(chuàng)建數(shù)據(jù)庫,如下:
- CREATE DATABASE `casbin_test` DEFAULT CHARACTER SET utf8;
 - 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`@`%`;
 - FLUSH PRIVILEGES;
 - DROP TABLE IF EXIST `casbin_rule`;
 - CREATE TABLE `casbin_rule` (
 - `p_type` varchar(100) DEFAULT NULL COMMENT '規(guī)則類型',
 - `v0` varchar(100) DEFAULT NULL COMMENT '角色I(xiàn)D',
 - `v1` varchar(100) DEFAULT NULL COMMENT 'api路徑',
 - `v2` varchar(100) DEFAULT NULL COMMENT 'api訪問方法',
 - `v3` varchar(100) DEFAULT NULL,
 - `v4` varchar(100) DEFAULT NULL,
 - `v5` varchar(100) DEFAULT NULL
 - ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='權(quán)限規(guī)則表';
 - /*插入操作casbin api的權(quán)限規(guī)則*/
 - INSERT INTO `casbin_rule`(`p_type`, `v0`, `v1`, `v2`) VALUES ('p', 'admin', '/api/v1/casbin', 'POST');
 - INSERT INTO `casbin_rule`(`p_type`, `v0`, `v1`, `v2`) VALUES ('p', 'admin', '/api/v1/casbin/list', 'GET');
 
代碼開發(fā)
由于代碼比較多,這里就不貼全部代碼了,全部代碼已經(jīng)放在gitee倉庫[3],可以自行閱讀,這些僅僅貼部分關(guān)鍵代碼。
(1)首先在configs目錄下創(chuàng)建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目錄下,創(chuàng)建casbin.go文件,寫入如下代碼:
- type CasbinModel struct {
 - PType string `json:"p_type" gorm:"column:p_type" description:"策略類型"`
 - RoleId string `json:"role_id" gorm:"column:v0" description:"角色I(xiàn)D"`
 - Path string `json:"path" gorm:"column:v1" description:"api路徑"`
 - Method string `json:"method" gorm:"column:v2" description:"訪問方法"`
 - }
 - func (c *CasbinModel) TableName() string {
 - return "casbin_rule"
 - }
 - func (c *CasbinModel) Create(db *gorm.DB) error {
 - e := Casbin()
 - if success := e.AddPolicy(c.RoleId,c.Path,c.Method); success == false {
 - return errors.New("存在相同的API,添加失敗")
 - }
 - return nil
 - }
 - func (c *CasbinModel) Update(db *gorm.DB, values interface{}) error {
 - if err := db.Model(c).Where("v1 = ? AND v2 = ?", c.Path, c.Method).Update(values).Error; err != nil {
 - return err
 - }
 - return nil
 - }
 - func (c *CasbinModel) List(db *gorm.DB) [][]string {
 - e := Casbin()
 - policy := e.GetFilteredPolicy(0, c.RoleId)
 - return policy
 - }
 - //@function: Casbin
 - //@description: 持久化到數(shù)據(jù)庫 引入自定義規(guī)則
 - //@return: *casbin.Enforcer
 - func Casbin() *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()
 - return enforcer
 - }
 - //@function: ParamsMatch
 - //@description: 自定義規(guī)則函數(shù)
 - //@param: fullNameKey1 string, key2 string
 - //@return: bool
 - func ParamsMatch(fullNameKey1 string, key2 string) bool {
 - key1 := strings.Split(fullNameKey1, "?")[0]
 - // 剝離路徑后再使用casbin的keyMatch2
 - return util.KeyMatch2(key1, key2)
 - }
 - //@function: ParamsMatchFunc
 - //@description: 自定義規(guī)則函數(shù)
 - //@param: args ...interface{}
 - //@return: interface{}, error
 - func ParamsMatchFunc(args ...interface{}) (interface{}, error) {
 - name1 := args[0].(string)
 - name2 := args[1].(string)
 - return ParamsMatch(name1, name2), nil
 - }
 
(3)在internal/dao目錄下創(chuàng)建casbin.go,寫入如下代碼:
- func (d *Dao) CasbinCreate(roleId string, path, method string) error {
 - cm := model.CasbinModel{
 - PType: "p",
 - RoleId: roleId,
 - Path: path,
 - Method: method,
 - }
 - return cm.Create(d.engine)
 - }
 - func (d *Dao) CasbinList(roleID string) [][]string {
 - cm := model.CasbinModel{RoleId: roleID}
 - return cm.List(d.engine)
 - }
 
(4)在internal/service目錄下創(chuàng)建service.go,寫入如下代碼:
- type CasbinInfo struct {
 - Path string `json:"path" form:"path"`
 - Method string `json:"method" form:"method"`
 - }
 - type CasbinCreateRequest struct {
 - RoleId string `json:"role_id" form:"role_id" description:"角色I(xiàn)D"`
 - CasbinInfos []CasbinInfo `json:"casbin_infos" description:"權(quán)限模型列表"`
 - }
 - type CasbinListResponse struct {
 - List []CasbinInfo `json:"list" form:"list"`
 - }
 - type CasbinListRequest struct {
 - RoleID string `json:"role_id" form:"role_id"`
 - }
 - func (s Service) CasbinCreate(param *CasbinCreateRequest) error {
 - for _, v := range param.CasbinInfos {
 - err := s.dao.CasbinCreate(param.RoleId, v.Path, v.Method)
 - if err != nil {
 - return err
 - }
 - }
 - return nil
 - }
 - func (s Service) CasbinList(param *CasbinListRequest) [][]string {
 - return s.dao.CasbinList(param.RoleID)
 - }
 
(5)在internal/router/api/v1目錄下創(chuàng)建casbin.go,寫入如下代碼:
- type Casbin struct {
 - }
 - func NewCasbin() Casbin {
 - return Casbin{}
 - }
 - // Create godoc
 - // @Summary 新增權(quán)限
 - // @Description 新增權(quán)限
 - // @Tags 權(quán)限管理
 - // @Produce json
 - // @Security ApiKeyAuth
 - // @Param body body service.CasbinCreateRequest true "body"
 - // @Success 200 {object} string "成功"
 - // @Failure 400 {object} errcode.Error "請(qǐng)求錯(cuò)誤"
 - // @Failure 500 {object} errcode.Error "內(nèi)部錯(cuò)誤"
 - // @Router /api/v1/casbin [post]
 - func (c Casbin) Create(ctx *gin.Context) {
 - param := service.CasbinCreateRequest{}
 - response := app.NewResponse(ctx)
 - valid, errors := app.BindAndValid(ctx, ¶m)
 - if !valid {
 - log.Printf("app.BindAndValid errs: %v", errors)
 - errRsp := errcode.InvalidParams.WithDetails(errors.Errors()...)
 - response.ToErrorResponse(errRsp)
 - return
 - }
 - // 進(jìn)行插入操作
 - svc := service.NewService(ctx)
 - err := svc.CasbinCreate(¶m)
 - if err != nil {
 - log.Printf("svc.CasbinCreate err: %v", err)
 - response.ToErrorResponse(errcode.ErrorCasbinCreateFail)
 - }
 - response.ToResponse(gin.H{})
 - return
 - }
 - // List godoc
 - // @Summary 獲取權(quán)限列表
 - // @Produce json
 - // @Tags 權(quán)限管理
 - // @Security ApiKeyAuth
 - // @Param data body service.CasbinListRequest true "角色I(xiàn)D"
 - // @Success 200 {object} service.CasbinListResponse "成功"
 - // @Failure 400 {object} errcode.Error "請(qǐng)求錯(cuò)誤"
 - // @Failure 500 {object} errcode.Error "內(nèi)部錯(cuò)誤"
 - // @Router /api/v1/casbin/list [post]
 - func (c Casbin) List(ctx *gin.Context) {
 - param := service.CasbinListRequest{}
 - response := app.NewResponse(ctx)
 - valid, errors := app.BindAndValid(ctx, ¶m)
 - if !valid {
 - log.Printf("app.BindAndValid errs: %v", errors)
 - errRsp := errcode.InvalidParams.WithDetails(errors.Errors()...)
 - response.ToErrorResponse(errRsp)
 - return
 - }
 - // 業(yè)務(wù)邏輯處理
 - svc := service.NewService(ctx)
 - casbins := svc.CasbinList(¶m)
 - var respList []service.CasbinInfo
 - for _, host := range casbins {
 - respList = append(respList, service.CasbinInfo{
 - Path: host[1],
 - Method: host[2],
 - })
 - }
 - response.ToResponseList(respList, 0)
 - return
 - }
 
再在該目錄下創(chuàng)建一個(gè)test.go文件,用于測(cè)試,代碼如下:
- type Test struct {
 - }
 - func NewTest() Test {
 - return Test{}
 - }
 - func (t Test) Get(ctx *gin.Context) {
 - log.Println("Hello 接收到GET請(qǐng)求..")
 - response := app.NewResponse(ctx)
 - response.ToResponse("接收GET請(qǐng)求成功")
 - }
 
(6)在internal/middleware目錄下創(chuàng)建casbin_handler.go,寫入如下代碼:
- func CasbinHandler() gin.HandlerFunc {
 - return func(ctx *gin.Context) {
 - response := app.NewResponse(ctx)
 - // 獲取請(qǐng)求的URI
 - obj := ctx.Request.URL.RequestURI()
 - // 獲取請(qǐng)求方法
 - act := ctx.Request.Method
 - // 獲取用戶的角色
 - sub := "admin"
 - e := model.Casbin()
 - fmt.Println(obj, act, sub)
 - // 判斷策略中是否存在
 - success := e.Enforce(sub, obj, act)
 - if success {
 - log.Println("恭喜您,權(quán)限驗(yàn)證通過")
 - ctx.Next()
 - } else {
 - log.Printf("e.Enforce err: %s", "很遺憾,權(quán)限驗(yàn)證沒有通過")
 - response.ToErrorResponse(errcode.UnauthorizedAuthFail)
 - ctx.Abort()
 - return
 - }
 - }
 - }
 
(7)在internal/router目錄下創(chuàng)建router.go,定義路由,代碼如下:
- func NewRouter() *gin.Engine {
 - r := gin.New()
 - r.Use(gin.Logger())
 - r.Use(gin.Recovery())
 - casbin := v1.NewCasbin()
 - test := v1.NewTest()
 - apiv1 := r.Group("/api/v1")
 - apiv1.Use(middleware.CasbinHandler())
 - {
 - // 測(cè)試路由
 - apiv1.GET("/hello", test.Get)
 - // 權(quán)限策略管理
 - apiv1.POST("/casbin", casbin.Create)
 - apiv1.POST("/casbin/list", casbin.List)
 - }
 - return r
 - }
 
最后就啟動(dòng)項(xiàng)目進(jìn)行測(cè)試。
驗(yàn)證
(1)首先訪問測(cè)試路徑,當(dāng)前情況下沒在權(quán)限表里,如下:

(2)將測(cè)試路徑添加到權(quán)限列表,如下:

(3)然后再次訪問測(cè)試路徑,如下:
并且從日志上也可以看到,如下:

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

















 
 
 




 
 
 
 