Gin集成Casbin進(jìn)行訪問權(quán)限控制
Casbin是什么
Casbin是一個(gè)強(qiáng)大的、高效的開源訪問控制框架,其權(quán)限管理機(jī)制支持多種訪問控制模型,Casbin只負(fù)責(zé)訪問控制[1]。
其功能有:
- 支持自定義請求的格式,默認(rèn)的請求格式為{subject, object, action}。.
- 具有訪問控制模型model和策略policy兩個(gè)核心概念。
- 支持RBAC中的多層角色繼承,不止主體可以有角色,資源也可以具有角色。
- 支持內(nèi)置的超級(jí)用戶 例如:root或administrator。超級(jí)用戶可以執(zhí)行任何操作而無需顯式的權(quán)限聲明。
- 支持多種內(nèi)置的操作符,如 keyMatch,方便對路徑式的資源進(jìn)行管理,如 /foo/bar可以映射到 /foo*
Casbin的工作原理
在 Casbin 中, 訪問控制模型被抽象為基于 **PERM **(Policy, Effect, Request, Matcher) [策略,效果,請求,匹配器]的一個(gè)文件。
- Policy:定義權(quán)限的規(guī)則
- Effect:定義組合了多個(gè)Policy之后的結(jié)果
- Request:訪問請求
- 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:是對policy生效范圍的定義,它對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ù)請求來匹配策略規(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ī)則很簡單,主要是選擇什么方式來存儲(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 "請求錯(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 "請求錯(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文件,用于測試,代碼如下:
- type Test struct {
- }
- func NewTest() Test {
- return Test{}
- }
- func (t Test) Get(ctx *gin.Context) {
- log.Println("Hello 接收到GET請求..")
- response := app.NewResponse(ctx)
- response.ToResponse("接收GET請求成功")
- }
(6)在internal/middleware目錄下創(chuàng)建casbin_handler.go,寫入如下代碼:
- func CasbinHandler() gin.HandlerFunc {
- return func(ctx *gin.Context) {
- response := app.NewResponse(ctx)
- // 獲取請求的URI
- obj := ctx.Request.URL.RequestURI()
- // 獲取請求方法
- 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())
- {
- // 測試路由
- 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)行測試。
驗(yàn)證
(1)首先訪問測試路徑,當(dāng)前情況下沒在權(quán)限表里,如下:

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

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

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