Go语言用 Gin + Casbin 实现灵活的 RBAC 权限控制

1,624 阅读2分钟

最近在写一个权限管理系统,需要实现基于角色(RBAC)的 RESTful API 权限控制,经过一番调研后选择了基于 Gin + Casbin 的方案。

需求分析

对于一个标准的权限管理系统,通常需要解决以下几个问题:

  1. 角色的继承关系
  2. RESTful API 的路径匹配
  3. HTTP 方法级别的控制
  4. 平台模式(单租户/多租户)的支持

Casbin 作为一个功能成熟的权限管理框架,刚好满足了这些需求。

Casbin 模型定义

先来看看基本的权限模型定义:

[request_definition]
r = mod, sub, obj, act

[policy_definition]
p = mod, sub, obj, act, eft

[role_definition]
g = *, *

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = r.sub == "root" || \
( g(r.sub, p.sub) && \
  g(r.mod, p.mod) && \
  (r.obj == p.obj || keyMatch2(r.obj, p.obj) || keyMatch(r.obj, p.obj) || p.obj == "*") && \
  (r.act == p.act || p.act == "*") )

这个模型比较有意思的地方在于:

  1. 使用 mod 字段支持平台模式的继承
  2. 通过 keyMatch2 实现了 RESTful 路径的匹配
  3. 支持通配符 * 进行批量授权

权限规则配置

有了模型后,我们还需要定义具体的权限规则:


# 角色继承
g, admin, network_admin
g, network_admin, user
g, user, *

# 平台模式
g, MultiTenantCrossPlatform, MultiTenantSinglePlatform
g, MultiTenantSinglePlatform, SingleTenantCrossPlatform
g, SingleTenantCrossPlatform, SingleTenantSinglePlatform

# 无认证路由
p, SingleTenantSinglePlatform, *, /healths, GET, allow
p, SingleTenantSinglePlatform, *, /login, POST, allow

# 用户相关路由
p, MultiTenantSinglePlatform, root, /organizations/:organizationId/users, GET, allow
p, SingleTenantSinglePlatform, admin, /users, GET, allow
p, SingleTenantSinglePlatform, user, /user, GET, allow

Go 代码实现

首先将配置文件嵌入到代码中:

package routers

import "embed"

//go:embed model.conf policy.csv
var fs embed.FS

func MustAssetString(name string) string {
    s, err := fs.ReadFile(name)
    if err != nil {
        panic(err)
    }
    return string(s)
}

然后实现 Gin 的认证中间件:

type Auth struct {
    Enforcer *casbin.Enforcer
}

func NewAuth() *Auth {
    m := model.NewModel()
    err := m.LoadModelFromText(routers.MustAssetString("model.conf"))
    if err != nil {
        logrus.Panicf("load casbin model failed: %v", err)
    }
    
    enforcer, err := casbin.NewEnforcer(m, textadapter.NewAdapter(routers.MustAssetString("policy.csv")))
    if err != nil {
        logrus.Panicf("new enforcer failed: %v", err)
    }
    
    return &Auth{Enforcer: enforcer}
}

中间件的处理逻辑:

func (a *Auth) HandlerFunc() gin.HandlerFunc {
    return func(c *gin.Context) {
        user, valid, err := a.validateByAuthorization(c)
        if err != nil {
            c.Render(err)
            c.Abort()
            return
        }
        
        if !valid {
            user, valid, err = a.validateByCookie(c)
            if err != nil {
                c.Render(err)
                c.Abort()
                return
            }
        }

        // 权限检查
        if valid {
            if ok, _ := a.checkPermission(user, c.Request.URL.Path, c.Request.Method); ok {
                c.Next()
                return
            }
        } else {
            if ok, _ := a.checkPublicAccess(c.Request.URL.Path, c.Request.Method); ok {
                c.Next()
                return
            }
        }
        
        c.Render(401)
        c.Abort()
    }
}

func (a *Auth) checkPermission(user *models.User, path, method string) (bool, error) {
    return a.Enforcer.Enforce(
        systems.GetPlatformMode(),
        fmt.Sprintf("%v", user.Role),
        path,
        method,
    )
}

func (a *Auth) checkPublicAccess(path, method string) (bool, error) {
    return a.Enforcer.Enforce(
        systems.GetPlatformMode(),
        "*",
        path,
        method,
    )
}

一些小技巧

  1. 使用 embed 将配置文件编译到二进制中,避免部署时的文件管理问题
  2. 权限检查前先判断是否为公共接口,可以减少一次权限查询
  3. 对于常用的权限组合,可以使用角色继承来简化配置
  4. 使用 keyMatch2 支持路径参数的匹配,比如 /users/:id

总结

Casbin + Gin 的组合可以很好地实现 RBAC 权限控制,关键是:

  1. 设计合理的权限模型
  2. 规划好角色继承关系

后续计划添加基于 Redis 的缓存层,以及支持动态更新权限规则的功能。

这是我的一些实践经验,如果你有更好的想法,欢迎一起探讨。

相关链接