gin框架中集成casbin-权限管理
阅读原文时间:2023年07月10日阅读:1

概念

权限管理几乎是每个系统或者服务都会直接或者间接涉及的部分. 权限管理保障了资源(大部分时候就是数据)的安全, 权限管理一般都是和业务强关联, 每当有新的业务或者业务变化时, 不能将精力完全放在业务实现上, 权限的调整往往耗费大量的精力. 其实, 权限的本质没有那么复杂, 只是对访问的控制而已, 有一套完善的访问控制接口, 再加上简单的权限模型. 权限模型之所以能够简单, 就是因为权限管理本身并不复杂, 只是在和具体业务结合时, 出现了各种各样的访问控制场景, 才显得复杂.

PERM 模型

PERM(Policy, Effect, Request, Matchers)模型很简单, 但是反映了权限的本质 – 访问控制

  1. Policy: 定义权限的规则
  2. Effect: 定义组合了多个 Policy 之后的结果, allow/deny
  3. Request: 访问请求, 也就是谁想操作什么
  4. Matcher: 判断 Request 是否满足 Policy

casbin的作用

  1. 以经典{subject, object, action}形式或您定义的自定义形式实施策略,同时支持允许和拒绝授权。
  2. 处理访问控制模型及其策略的存储。
  3. 管理角色用户映射和角色角色映射(RBAC中的角色层次结构)。
  4. 支持内置的超级用户,例如root或administrator。超级用户可以在没有显式权限的情况下执行任何操作。
  5. 多个内置运算符支持规则匹配。例如,keyMatch可以将资源键映射/foo/bar到模式/foo*。

casbin不执行的操作

  1. 身份验证(又名验证username以及password用户登录时)
  2. 管理用户或角色列表。我相信项目本身管理这些实体会更方便。用户通常具有其密码,而Casbin并非设计为密码容器。但是,Casbin存储RBAC方案的用户角色映射。

在使用Casbin 控制后台接口时使用以下模型

[request_definition]
    r = sub, obj, act
# 请求的规则
# r 是规则的名称,sub 为请求的实体,obj 为资源的名称, act 为请求的实际操作动作
[policy_definition]
    p = sub, obj, act
# 策略的规则
# 同请求
[role_definition]
    g = _, _
# 角色的定义
# g 角色的名称,第一个位置为用户,第二个位置为角色,第三个位置为域(在多租户场景下使用)
[policy_effect]
    e = some(where (p.eft == allow))
# 任意一条 policy rule 满足, 则最终结果为 allow
[matchers]
    m = g(r.sub, p.sub) == true \
     && keyMatch2(r.obj, p.obj) == true \
      && regexMatch(r.act, p.act) == true \
      || r.sub == "root"
# [matchers] 也可以这样写
# m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act || r.sub == "root"
# 前三个用来匹配上面定义的请求的规则, 最后一个或条件为:如果实体是root 直接通过, 不验证权限

RBAC模型的示例策略如下:

p, cityAdmin, /city, GET
p, cityAdmin, /city, POST
p, countyAdmin, /county, GET
g, mayanan, superAdmin

在理解了Casbin 的工作原理后,实际写代码测试一下

需要使用的外部包

go get -u github.com/casbin/casbin  Casbin 官方库
go get -u github.com/casbin/gorm-adapter  Casbin 插件,用来将规则和策略保存到数据库中
go get -u github.com/gin-gonic/gin  Go Web 框架
go get -u github.com/go-sql-driver/mysql  Go MySQL 驱动 

方案一

参考链接

点击查看代码

package main

import (
    "fmt"
    "github.com/casbin/casbin"
    "github.com/casbin/gorm-adapter"
    "github.com/gin-gonic/gin"
    _ "github.com/go-sql-driver/mysql"
    "net/http"
)

func main() {
    // 要使用自己定义的数据库rbac_db,最后的true很重要.默认为false,使用缺省的数据库名casbin,不存在则创建
    a := gormadapter.NewAdapter("mysql", "root:pwdZ@tcp(rm-xxxx.mysql.rds.aliyuncs.com:33016)/my_casbin?charset=utf8mb4&parseTime=True&loc=Local", true)
    e := casbin.NewEnforcer("./test/model.conf", a)
    // 从DB加载策略
    e.LoadPolicy()

    //获取router路由对象
    r := gin.New()

    r.POST("/api/v1/add", func(c *gin.Context) {
        fmt.Println("增加Policy")
        // AddPolicy 向当前策略添加授权规则。如果规则已经存在,函数返回false,不会添加规则。否则,该函数通过添加新规则返回 true
        if ok := e.AddPolicy("admin", "/api/v1/hello", "GET"); !ok {
            fmt.Println("Policy已经存在")
        } else {
            fmt.Println("增加成功")
        }
    })
    //删除policy
    r.DELETE("/api/v1/delete", func(c *gin.Context) {
        fmt.Println("删除Policy")
        // RemovePolicy 从当前策略中删除授权规则。
        if ok := e.RemovePolicy("admin", "/api/v1/hello", "GET"); !ok {
            fmt.Println("Policy不存在")
        } else {
            fmt.Println("删除成功")
        }
    })
    //获取policy
    r.GET("/api/v1/get", func(c *gin.Context) {
        fmt.Println("查看policy")
        // GetPolicy 获取策略中的所有授权规则。
        list := e.GetPolicy()
        for _, vlist := range list {
            for _, v := range vlist {
                fmt.Printf("value: %s, ", v)
            }
        }
    })
    //使用自定义拦截器中间件
    r.Use(Authorize(e))
    //创建请求
    r.GET("/api/v1/hello", func(c *gin.Context) {
        fmt.Println("Hello 接收到GET请求..")
    })

    r.Run(":9000") //参数为空 默认监听8080端口
}

//拦截器
func Authorize(e *casbin.Enforcer) gin.HandlerFunc {
    return func(c *gin.Context) {
        //获取请求的URI
        obj := c.Request.URL.RequestURI()
        //获取请求方法
        act := c.Request.Method
        //获取用户的角色
        sub := "admin"

        //判断策略中是否存在
        if ok := e.Enforce(sub, obj, act); ok {
            fmt.Println("恭喜您,权限验证通过")
            c.Next()
        } else {
            fmt.Println("很遗憾,权限验证没有通过")
            c.Abort()
            c.String(http.StatusUnauthorized, "无权访问")
        }
    }
} 

方案二:

参考链接

点击查看代码

package main

import (
    "fmt"
    "github.com/casbin/casbin"
    "github.com/casbin/gorm-adapter"
    "github.com/gin-gonic/gin"
    _ "github.com/go-sql-driver/mysql"
    "github.com/jinzhu/gorm"
    "net/http"

    //"gorm.io/gorm"
)

// 统一响应结构体
type Response struct {
    Code    int         `json:"code"`
    Message string      `json:"message"`
    Data    interface{} `json:"data"`
}

var O *gorm.DB
var PO *gormadapter.Adapter
var Enforcer *casbin.Enforcer

func ping(c *gin.Context) {
    var response Response
    response.Code = 0
    response.Message = "success"
    response.Data = ""
    c.JSON(200, response)
    return
}

// 数据库连接及角色规则的初始化
func connect() {
    dsn := "root:xxx@tcp(rm-xxx.mysql.rds.aliyuncs.com:33016)/my_casbin?charset=utf8mb4&parseTime=True&loc=Local"
    var err error
    O, err = gorm.Open("mysql", dsn)
    if err != nil {
        fmt.Println("connect DB error")
        panic(err)
    }
    // 将数据库连接同步给插件, 插件用来操作数据库
    PO = gormadapter.NewAdapterByDB(O)
    // 这里也可以使用原生字符串方式
    Enforcer = casbin.NewEnforcer("./test/model.conf", PO)
    // 开启权限认证日志
    Enforcer.EnableLog(true)
    // 加载数据库中的策略
    err = Enforcer.LoadPolicy()
    if err != nil {
        fmt.Println("loadPolicy error")
        panic(err)
    }
    // 创建一个角色,并赋于权限
    // admin 这个角色可以用 GET 方式访问 /api/v2/ping
    res := Enforcer.AddPolicy("admin", "/api/v2/ping", "GET")
    if !res {
        fmt.Println("policy is exist")
    } else {
        fmt.Println("policy is not exist, adding")
    }
    // 将 test 用户加入一个角色中
    Enforcer.AddRoleForUser("test", "root")
    Enforcer.AddRoleForUser("tom", "admin")
    // 请看规则中如果用户名为 root 则不受限制
}

func main() {
    defer O.Close()
    connect()
    g := gin.Default()
    // 这里的接口没有使用权限认证中间件
    version1 := g.Group("/api/v1")
    {
        version1.GET("/ping", ping) // 这个是通用的接口
    }
    // 接口使用权限认证中间件
    version2 := g.Group("/api/v2", CasbinMiddleWare)
    {
        version2.GET("/ping", ping)
    }
    _ = g.Run(":8099")
}

// casbin middleware 权限认证中间件
func CasbinMiddleWare(c *gin.Context) {
    var userName string
    userName = c.GetHeader("userName")
    if userName == "" {
        fmt.Println("headers invalid")
        c.JSON(200, gin.H{
            "code":    http.StatusUnauthorized,
            "message": "Unauthorized",
            "data":    "",
        })
        c.Abort()
        return
    }
    // 请求的path
    p := c.Request.URL.Path
    // 请求的方法
    m := c.Request.Method
    // 这里认证
    res, err := Enforcer.EnforceSafe(userName, p, m)
    // 这个 HasPermissionForUser 跟上面的有什么区别
    // EnforceSafe 会验证角色的相关的权限
    // 而 HasPermissionForUser 只验证用户是否有权限
    //res = Enforcer.HasPermissionForUser(userName,p,m)
    if err != nil {
        fmt.Println("no permission ")
        fmt.Println(err)
        c.JSON(200, gin.H{
            "code":    401,
            "message": "Unauthorized",
            "data":    "",
        })
        c.Abort()
        return
    }
    if !res {
        fmt.Println("permission check failed")
        c.JSON(200, gin.H{
            "code":    401,
            "message": "Unauthorized",
            "data":    "",
        })
        c.Abort()
        return
    }
    c.Next()
} 

  • 结果

    p 代表的是策略 admin 角色可以使用GET 访问 /api/v2/ping

    g 代表的是角色 test 用户在root 角色中

    tom 在admin 角色中

    所以在测试时请求头

    userName = root 有正常响应 (这里不会到数据库验证,策略最后一条)

    userName = tom 正常响应 (tom 有admin 角色 , 所以验证通过)

    userName = role_admin 正常响应 (参考这里:https://casbin.org/docs/zh-CN/rbac , 正常情况下用户名和角色名称不应该一样)

    userName = *** 都无法通过认证

RBAC API 官网

RBAC API 官网

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章