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

你可能感兴趣的:(Go语言 gin框架集成Casbin访问权限控制)