Go語言 gin框架集成Casbin訪問權限控制

1. Casbin是什么?

Casbin是一個強大的、高效的開源訪問控制框架,其權限管理機制支持多種訪問控制模型。因此Casbin不能做身份驗證, 最佳的實踐是只負責訪問控制

1.1 Casbin的model

Casbin 中, 訪問控制模型被抽象為基于 PERM (Policy, Effect, Request, Matcher) 的一個文件,這個文件的具體呈現是一個以 .conf 作為后綴的文件

example :

rbac_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

對于上面配置文件簡單的理解是:


1.1.1 [request_definition]

r = sub, obj, act :定義請求由三部分組成 訪問用戶的用戶 Subject , 訪問的資源 Object 訪問的動作 Action

1.1.2 [policy_definition]

p = sub, obj, act : 定策略的格式 , 參數的基本意思和定義請求的相同 ,定義好了策略格式,那么對于策略(Policy)的具體描述可以存放在一個以 .csv 作為后綴的文件中

example :

rbac_Policy_example.csv

g, coder, root
g, zhangsan coder
p, root,api/v1/ping,GET
p, coder,api/v1/pong,GET
g, lisi, manager
p, manager, api/v1/user,POST

上面的rbac策略中我們定義了三條策略和三個用戶組,我們來看一下這些策略都有啥作用

  1. coder是root的角色
  2. zhangsan是coder的角色
  3. root 可以訪問 api/v1/ping 資源 通過GET動作,那么coder , zhangsan也可以訪問
  4. coder可以訪問 api/v1/pong 資源 通過GET動作,zhangsan也能訪問
  5. lisi是manager的角色
  6. manager可以訪問 api/v1/user資源通過POST動作,lisi也可以訪問
1.1.3 [role_definition]

**g = _, _ ** : 是RBAC角色繼承關系的定義 ,此處的 _, _ 表示 前項繼承后項角色的權限

1.1.4 [policy_effect]

e = some(where (p.eft == allow)) : 表示任意一條Policy策略滿足那么結果就為allow

1.1.5 [matchers]

m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act : 定義了策略匹配者。匹配者是一組表達式。它定義了如何根據請求來匹配策略規則,匹配表達式的寫法比較靈活根據具體需求來編寫即可.
而此處的表達式意思是 ,檢測用戶角色 && 檢測用戶訪問的資源 &&檢測用戶的動作 (&&表示并且關系,當然也有其他邏輯運算符 ||,!等)

1.2 Casbin的Policy

Policy 主要表示訪問控制關于角色,資源,行為的具體映射關系這比較好處理,但是這種映射關系怎么存儲就值得考慮了

1.2.1 csv 文件存儲
訪問控制模型 Model 文件 Policy 文件
ACL basic_model.conf basic_policy.csv
具有超級用戶的ACL basic_with_root_model.conf basic_policy.csv
沒有用戶的ACL basic_without_users_model.conf basic_without_users_policy.csv
沒有資源的ACL basic_without_resources_model.conf basic_without_resources_policy.csv
RBAC rbac_model.conf rbac_policy.csv
支持資源角色的RBAC rbac_with_resource_roles_model.conf rbac_with_resource_roles_policy.csv
支持域/租戶的RBAC rbac_with_domains_model.conf rbac_with_domains_policy.csv
ABAC abac_model.conf
RESTful keymatch_model.conf keymatch_policy.csv
拒絕優先 rbac_with_not_deny_model.conf rbac_with_deny_policy.csv
Allow-and-deny rbac_with_deny_model.conf rbac_with_deny_policy.csv
Priority priority_model.conf priority_policy.csv
1.2.2 適配器存儲

casbin的適配器 adapter 可以從存儲中加載策略規則,也可將策略規則保存到不同的存儲系統中

支持如: MySQL, PostgreSQL, SQL Server, SQLite3,MongoDB,Redis,Cassandra DB等等存儲系統

適配器 類型 作者 自動保存 描述
File Adapter (內置) File Casbin ? For .CSV (Comma-Separated Values) files
Filtered File Adapter (內置) File @faceless-saint ? For .CSV (Comma-Separated Values) files with policy subset loading support
SQL Adapter SQL @Blank-Xu ? MySQL, PostgreSQL, SQL Server, SQLite3 are supported in master branch and Oracle is supported in oracle branch by database/sql
Xorm Adapter ORM Casbin ? MySQL, PostgreSQL, TiDB, SQLite, SQL Server, Oracle are supported by Xorm
Gorm Adapter ORM Casbin ? MySQL, PostgreSQL, Sqlite3, SQL Server are supported by Gorm
Beego ORM Adapter ORM Casbin ? MySQL, PostgreSQL, Sqlite3 are supported by Beego ORM
SQLX Adapter ORM @memwey ? MySQL, PostgreSQL, SQLite, Oracle are supported by SQLX
Sqlx Adapter SQL @Blank-Xu ? MySQL, PostgreSQL, SQL Server, SQLite3 are supported in master branch and Oracle is supported in oracle branch by sqlx
GF ORM Adapter ORM @vance-liu ? MySQL, SQLite, PostgreSQL, Oracle, SQL Server are supported by GF ORM
Filtered PostgreSQL Adapter SQL Casbin ? For PostgreSQL
PostgreSQL Adapter SQL @cychiuae ? For PostgreSQL
PostgreSQL Adapter (Archived) SQL Going ? For PostgreSQL
RQLite Adapter SQL EDOMO Systems ? For RQLite
MongoDB Adapter NoSQL Casbin ? For MongoDB based on MongoDB driver for Go
MongoDB Adapter NoSQL Titan DC ? For MongoDB based on MongoDB Go driver
RethinkDB Adapter NoSQL @adityapandey9 ? For RethinkDB
Cassandra Adapter NoSQL Casbin ? For Apache Cassandra DB
DynamoDB Adapter NoSQL HOOQ ? For Amazon DynamoDB
Dynacasbin NoSQL NewbMiao ? For Amazon DynamoDB
ArangoDB Adapter NoSQL @adamwasila ? For ArangoDB
Amazon S3 Adapter Cloud Soluto ? For Minio and Amazon S3
Azure Cosmos DB Adapter Cloud @spacycoder ? For Microsoft Azure Cosmos DB
GCP Datastore Adapter Cloud LivingPackets ? For Google Cloud Platform Datastore
GCP Firestore Adapter Cloud @reedom ? For Google Cloud Platform Firestore
Consul Adapter KV store @ankitm123 ? For HashiCorp Consul
Redis Adapter KV store Casbin ? For Redis
Etcd Adapter KV store @sebastianliu ? For etcd
BoltDB Adapter KV store @speza ? For Bolt
Bolt Adapter KV store @wirepair ? For Bolt
BadgerDB Adapter KV store @inits ? For BadgerDB
Protobuf Adapter Stream Casbin ? For Google Protocol Buffers
JSON Adapter String Casbin ? For JSON
String Adapter String @qiangmzsx ? For String

2. gin集成Casbin實現RESTful接口訪問控制

2.1 go mod 構建項目

# 新建個叫做ginCasbin的gomod項目(項目名自定義)
go mod init GinCasbin

2.2 安裝依賴包

# 安裝依賴包
# 安裝gin框架
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驅動依賴
go get github.com/go-sql-driver/mysql
# gorm 包
go get github.com/jinzhu/gorm
# 高性能緩存BigCache
go get github.com/allegro/bigcache/v2

2.3 目錄規劃說明

├─app # 業務目錄
│  ├─api  ## 存放api的目錄(暫時不用)
│  ├─model ## 存放實體的目錄(暫時不用)
│  └─service ## 存放業務代碼的目錄(暫時不用)
├─config # 存放配置文件的目錄
├─middleware # 存放中間件的目錄
├─routers # 存放路由的目錄
└─utils # 常用工具組件目錄
    ├─ACS ## 存放訪問控制執行器目錄
    ├─APIResponse ##  存放API統一響應函數目錄
    ├─Cache ## 緩存工具目錄
    └─DB ## 數據連接文件目錄
├─go.mod
├─go.sum
├─main.go # 項目入口文件

2.4 項目代碼開發

2.4.1 工具組件開發
# 進入utils目錄
cd utils

DB/mysql.go

package DB

import (
    "fmt"
    "github.com/jinzhu/gorm"
)
import _ "github.com/go-sql-driver/mysql"

var (
    Mysql *gorm.DB
)

func init() {
    var err error
    dsn := "root:root@(127.0.0.1:3306)/xz_boss?charset=utf8&parseTime=True&loc=Local"
    Mysql, err = gorm.Open("mysql", dsn)
    if err != nil {
        fmt.Println("connect DB error")
        panic(err)
    }
}

ACS/enforcer.go

package ACS

import (
    "GinCasbin/utils/DB"
    "github.com/casbin/casbin"
    "github.com/casbin/gorm-adapter"
)

var Enforcer *casbin.Enforcer

func init() {
    // mysql 適配器
    adapter := gormadapter.NewAdapterByDB(DB.Mysql)
    // 通過mysql適配器新建一個enforcer
    Enforcer = casbin.NewEnforcer("config/keymatch2_model.conf", adapter)
    // 日志記錄
    Enforcer.EnableLog(true)
}


APIResponse/response.go

package APIResponse

import "github.com/gin-gonic/gin"

type Response struct {
    Code    int         `json:"code"`
    Message string      `json:"message"`
    Data    interface{} `json:"data"`
}

var C *gin.Context

func Error(message string) {
    if len(message) == 0 {
        message = "fail"
    }
    C.JSON(200, Response{
        Code:    -1,
        Message: message,
        Data:    nil,
    })
}
func Success(data interface{}) {
    C.JSON(200, Response{
        Code:    200,
        Message: "success",
        Data:    data,
    })
}

Cache/big.go

package Cache

import (
    "github.com/allegro/bigcache/v2"
    "time"
)

var GlobalCache *bigcache.BigCache

func init() {
    // 初始化BigCache實例
    GlobalCache, _ = bigcache.NewBigCache(bigcache.DefaultConfig(30 * time.Minute))
}

2.4.2 配置文件

常規項目中配置文件目錄中會存放各種配置文件,在這個Demo中僅將casbin的模型文件放在這里

cd ../config

config/keymatch2_model.conf

[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) && regexMatch(r.act, p.act)
2.4.3 中間件

此處我們編寫的一個基于casbin權限控制的中間件

cd ../middleware

middleware/privilege.go

package middleware

import (
    "GinCasbin/utils/ACS"
    "GinCasbin/utils/APIResponse"
    "GinCasbin/utils/Cache"
    "github.com/gin-gonic/gin"
    "log"
)

func Privilege() gin.HandlerFunc {
    return func(c *gin.Context) {
        APIResponse.C = c
        var userName = c.GetHeader("userName")
        if userName == "" {
            APIResponse.Error("header miss userName")
            c.Abort()
            return
        }
        path := c.Request.URL.Path
        method := c.Request.Method
        cacheName := userName + path + method
        // 從緩存中讀取&判斷
        entry, err := Cache.GlobalCache.Get(cacheName)
        if err == nil && entry != nil {
            if string(entry) == "true" {
                c.Next()
            } else {
                APIResponse.Error("access denied")
                c.Abort()
                return
            }
        } else {
            // 從數據庫中讀取&判斷
            //記錄日志
            ACS.Enforcer.EnableLog(true)
            // 加載策略規則
            err := ACS.Enforcer.LoadPolicy()
            if err != nil {
                log.Println("loadPolicy error")
                panic(err)
            }
            // 驗證策略規則
            result, err := ACS.Enforcer.EnforceSafe(userName, path, method)
            if err != nil {
                APIResponse.Error("No permission found")
                c.Abort()
                return
            }
            if !result {
                // 添加到緩存中
                Cache.GlobalCache.Set(cacheName, []byte("false"))
                APIResponse.Error("access denied")
                c.Abort()
                return
            } else {
                Cache.GlobalCache.Set(cacheName, []byte("true"))
            }
            c.Next()
        }
    }
}

2.4.4 路由文件
cd ../routers

routers/route.go

package routers

import (
    "GinCasbin/middleware"
    "GinCasbin/utils/ACS"
    "GinCasbin/utils/APIResponse"
    "GinCasbin/utils/Cache"
    "github.com/gin-gonic/gin"
)

var (
    R *gin.Engine
)

func init() {
    R = gin.Default()
    R.NoRoute(func(c *gin.Context) {
        c.JSON(400, gin.H{"code": 400, "message": "Bad Request"})
    })
    api()
}
func api() {
    auth := R.Group("/api")
    {
        // 模擬添加一條Policy策略
        auth.POST("acs", func(c *gin.Context) {
            APIResponse.C = c
            subject := "tom"
            object := "/api/routers"
            action := "POST"
            cacheName := subject + object + action
            result := ACS.Enforcer.AddPolicy(subject, object, action)
            if result {
                // 清除緩存
                _ = Cache.GlobalCache.Delete(cacheName)
                APIResponse.Success("add success")
            } else {
                APIResponse.Error("add fail")
            }
        })
        // 模擬刪除一條Policy策略
        auth.DELETE("acs/:id", func(context *gin.Context) {
            APIResponse.C = context
            result := ACS.Enforcer.RemovePolicy("tom", "/api/routers", "POST")
            if result {
                // 清除緩存 代碼省略
                APIResponse.Success("delete Policy success")
            } else {
                APIResponse.Error("delete Policy fail")
            }
        })
        // 獲取路由列表
        auth.POST("/routers", middleware.Privilege(), func(c *gin.Context) {
            type data struct {
                Method string `json:"method"`
                Path   string `json:"path"`
            }
            var datas []data
            routers := R.Routes()
            for _, v := range routers {
                var temp data
                temp.Method = v.Method
                temp.Path = v.Path
                datas = append(datas, temp)
            }
            APIResponse.C = c
            APIResponse.Success(datas)
            return
        })
    }
    // 定義路由組
    user := R.Group("/api/v1")
    // 使用訪問控制中間件
    user.Use(middleware.Privilege())
    {
        user.POST("user", func(c *gin.Context) {
            c.JSON(200, gin.H{"code": 200, "message": "user add success"})
        })
        user.DELETE("user/:id", func(c *gin.Context) {
            id := c.Param("id")
            c.JSON(200, gin.H{"code": 200, "message": "user delete success " + id})
        })
        user.PUT("user/:id", func(c *gin.Context) {
            id := c.Param("id")
            c.JSON(200, gin.H{"code": 200, "message": "user update success " + id})
        })
        user.GET("user/:id", func(c *gin.Context) {
            id := c.Param("id")
            c.JSON(200, gin.H{"code": 200, "message": "user Get success " + id})
        })
    }
}

2.4.5 項目入口文件
cd ..

main.go

package main

import (
    . "GinCasbin/routers"
)

func main() {
    R.Run()
}

2.5 測試訪問策略

2.5.1 啟動項目
# 運行項目
go run main.go
# gin框架在debug模式下的輸出
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[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] POST   /api/acs                  --> GinCasbin/routers.api.func1 (3 handlers)
[GIN-debug] DELETE /api/acs/:id              --> GinCasbin/routers.api.func2 (3 handlers)
[GIN-debug] POST   /api/routers              --> GinCasbin/routers.api.func3 (4 handlers)
[GIN-debug] POST   /api/v1/user              --> GinCasbin/routers.api.func4 (4 handlers)
[GIN-debug] DELETE /api/v1/user/:id          --> GinCasbin/routers.api.func5 (4 handlers)
[GIN-debug] PUT    /api/v1/user/:id          --> GinCasbin/routers.api.func6 (4 handlers)
[GIN-debug] GET    /api/v1/user/:id          --> GinCasbin/routers.api.func7 (4 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080

2.5.2 測試casbin訪問控制

新開啟一個命令行終端

# 訪問接口
# 參數缺失
curl -X POST http://127.0.0.1:8080/api/routers
{"code":-1,"message":"header miss userName","data":null}


# 無訪問權限
curl -X POST -H "userName:tom" http://127.0.0.1:8080/api/routers
{"code":-1,"message":"access denied","data":null}


# 添加一條規則(代碼中是模擬數據)
curl -X POST http://127.0.0.1:8080/api/acs
{"code":200,"message":"success","data":"add success"}

# 再次訪問(有訪問權限,可以訪問)
curl -X POST -H "userName:tom" http://127.0.0.1:8080/api/routers
{
    "code":200,
    "message":"success",
    "data":[
        {
            "method":"POST",
            "path":"/api/acs"
        },
        {
            "method":"POST",
            "path":"/api/routers"
        },
        {
            "method":"POST",
            "path":"/api/v1/user"
        },
        {
            "method":"DELETE",
            "path":"/api/acs/:id"
        },
        {
            "method":"DELETE",
            "path":"/api/v1/user/:id"
        },
        {
            "method":"PUT",
            "path":"/api/v1/user/:id"
        },
        {
            "method":"GET",
            "path":"/api/v1/user/:id"
        }
    ]
}

# 直接向數據庫添加幾條Policy策略
INSERT INTO `xz_boss`.`casbin_rule` (`p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES ('p', 'admin', '/api/v1/user', 'POST', NULL, NULL, NULL);
INSERT INTO `xz_boss`.`casbin_rule` (`p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES ('p', 'admin', '/api/v1/user/:id', 'GET', NULL, NULL, NULL);
INSERT INTO `xz_boss`.`casbin_rule` (`p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES ('p', 'admin', '/api/v1/user/:id', 'PUT', NULL, NULL, NULL);

#再測試
## 添加接口
curl -X POST -H "userName:admin" http://127.0.0.1:8080/api/v1/user
{"code":200,"message":"user add success"}
## 查詢接口
curl -X GET -H "userName:admin" http://127.0.0.1:8080/api/v1/user/99
{"code":200,"message":"user Get success 99"}
## 更新接口
curl -X PUT -H "userName:admin" http://127.0.0.1:8080/api/v1/user/199
{"code":200,"message":"user update success 199"}
## 刪除接口(沒有分配訪問權限)
curl -X DELETE -H "userName:admin" http://127.0.0.1:8080/api/v1/user/299
{"code":-1,"message":"access denied","data":null}

2.6 其他

casbin的一些適配器有自動保存功能而另外一些則沒有,有自動保存功能的適配器會在連接數據的時候自動創建一張表用來保存Policy策略數據(替代存儲Policy的csv文件)

上述 Demo 的SQL文件如下(該表是gorm適配器自動創建的)

casbin_rule.sql

-- ----------------------------
-- Table structure for casbin_rule
-- ----------------------------
DROP TABLE IF EXISTS `casbin_rule`;
CREATE TABLE `casbin_rule` (
  `p_type` varchar(100) DEFAULT NULL,
  `v0` varchar(100) DEFAULT NULL,
  `v1` varchar(100) DEFAULT NULL,
  `v2` varchar(100) DEFAULT NULL,
  `v3` varchar(100) DEFAULT NULL,
  `v4` varchar(100) DEFAULT NULL,
  `v5` varchar(100) DEFAULT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of casbin_rule
-- ----------------------------
INSERT INTO `casbin_rule` VALUES ('p', 'zhangsan', '/api/v1/ping', 'GET', null, null, null);
INSERT INTO `casbin_rule` VALUES ('p', 'coder', '/api/v2/user/:id', 'GET', null, null, null);
INSERT INTO `casbin_rule` VALUES ('p', 'coder', '/api/v2/routers', 'GET', null, null, null);
INSERT INTO `casbin_rule` VALUES ('p', 'admin', '/api/v1/user', 'POST', null, null, null);
INSERT INTO `casbin_rule` VALUES ('p', 'admin', '/api/v1/user/:id', 'GET', null, null, null);
INSERT INTO `casbin_rule` VALUES ('p', 'admin', '/api/v1/user/:id', 'PUT', null, null, null);
INSERT INTO `casbin_rule` VALUES ('p', 'tom', '/api/routers', 'POST', '', '', '');

參考資料

- [1] casbin

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。