Go 项目中使用 Casbin 实现 RBAC 权限管理完整教程

Go 项目中使用 Casbin 实现 RBAC 权限管理完整教程

📖 前言

在构建企业级 Go 管理后台系统时,权限管理是一个核心功能。Casbin 是一个强大的、开源的访问控制库,支持多种访问控制模型(ACL、RBAC、ABAC 等)。本文将详细介绍如何在 Go 项目中使用 Casbin 实现完整的 RBAC(基于角色的访问控制)权限管理系统。

本文基于开源项目 gin-layout 的实际实现,这是一个功能完整的 Go Admin 后台管理系统框架,提供了开箱即用的权限管理解决方案。

🎯 什么是 Casbin?

Casbin 是一个强大的、开源的访问控制库,支持多种访问控制模型:

  • ACL (Access Control List) - 访问控制列表
  • RBAC (Role-Based Access Control) - 基于角色的访问控制
  • ABAC (Attribute-Based Access Control) - 基于属性的访问控制
  • RESTful - RESTful 风格的访问控制

Casbin 的核心思想是将访问控制模型与策略分离,通过配置文件定义访问控制模型,通过适配器(Adapter)存储策略规则。

📦 项目依赖

首先,我们需要安装 Casbin 相关的依赖包:

bash 复制代码
go get github.com/casbin/casbin/v2
go get github.com/casbin/gorm-adapter/v3

🔧 1. Casbin 模型配置

Casbin 使用模型文件(Model)来定义访问控制规则。在项目中,我们创建了 rbac_model.conf 文件:

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 = (g(r.sub, p.sub) && keyMatch3(r.obj, p.obj) && regexMatch(r.act, p.act))

配置说明

  • request_definition : 定义请求格式,r = sub, obj, act 表示请求包含主体(subject)、对象(object)和操作(action)
  • policy_definition : 定义策略格式,p = sub, obj, act 表示策略包含主体、对象和操作
  • role_definition : 定义角色继承关系,g = _, _ 表示角色继承关系(如 g, user:1, role:1 表示用户1继承角色1)
  • policy_effect : 定义策略效果,e = some(where (p.eft == allow)) 表示只要有一个策略允许就允许访问
  • matchers : 定义匹配规则
    • g(r.sub, p.sub): 检查请求主体是否继承策略主体(角色继承)
    • keyMatch3(r.obj, p.obj): 使用 keyMatch3 函数匹配路径(支持 * 通配符)
    • regexMatch(r.act, p.act): 使用正则表达式匹配 HTTP 方法

🚀 2. Casbin 初始化

在项目中,我们封装了一个 Casbin 工具包 internal/pkg/utils/casbin/casbin.go

go 复制代码
package casbinx

import (
    "github.com/casbin/casbin/v2"
    "github.com/casbin/casbin/v2/model"
    gormadapter "github.com/casbin/gorm-adapter/v3"
    "gorm.io/gorm"
)

type CasbinEnforcer struct {
    *casbin.Enforcer
    errInit error
    tx      *gorm.DB
    model   model.Model
}

var casbinx = &CasbinEnforcer{}

// InitEnforcer 初始化 Casbin Enforcer(仅执行一次)
func InitEnforcer() error {
    once.Do(func() {
        // 1. 加载模型文件
        modelPath, err := getModelPath()
        if err != nil {
            casbinx.errInit = err
            return
        }

        m, err := model.NewModelFromFile(modelPath)
        if err != nil {
            casbinx.errInit = fmt.Errorf("加载模型失败: %w", err)
            return
        }
        casbinx.model = m

        // 2. 创建 GORM 适配器
        db := data.MysqlDB()
        gormadapter.TurnOffAutoMigrate(db)
        adapter, err := gormadapter.NewAdapterByDB(db)
        if err != nil {
            casbinx.errInit = fmt.Errorf("创建适配器失败: %w", err)
            return
        }

        // 3. 创建 Enforcer
        enforcer, err := casbin.NewEnforcer(m, adapter)
        if err != nil {
            casbinx.errInit = fmt.Errorf("创建 Enforcer 失败: %w", err)
            return
        }

        // 4. 启用自动保存
        enforcer.EnableAutoSave(true)

        casbinx.Enforcer = enforcer
    })
    return casbinx.errInit
}

// GetEnforcer 返回已初始化的 Enforcer 实例
func GetEnforcer() *CasbinEnforcer {
    if casbinx.Enforcer == nil {
        if err := InitEnforcer(); err != nil {
            return nil
        }
    }
    return casbinx
}

关键点说明

  1. 单例模式 : 使用 sync.Once 确保 Enforcer 只初始化一次
  2. GORM 适配器 : 使用 gorm-adapter 将策略存储在 MySQL 数据库中
  3. 自动保存 : EnableAutoSave(true) 确保策略变更后自动保存到数据库
  4. 模型加载: 从配置文件加载访问控制模型

🔐 3. 权限检查中间件

在 Gin 框架中,我们通过中间件来实现权限检查:

go 复制代码
// internal/middleware/admin_auth.go

func AdminAuthHandler() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 1. 获取用户信息
        uid := c.GetUint("uid")
        if uid == 0 {
            response.Fail(c, e.NotLogin, "请先登录")
            c.Abort()
            return
        }

        adminUser := getUserFromContext(c)
        if adminUser == nil {
            response.Fail(c, e.NotLogin, "登录已失效,请重新登录")
            c.Abort()
            return
        }

        // 2. 超级管理员跳过权限检查
        if !isSuperAdmin(adminUser) {
            // 3. 检查接口权限
            if err := checkPermission(c, adminUser); err != nil {
                if businessErr, ok := err.(*e.BusinessError); ok {
                    response.Fail(c, businessErr.GetCode(), businessErr.GetMessage())
                } else {
                    response.Fail(c, e.ServerErr, "权限验证失败")
                }
                c.Abort()
                return
            }
        }

        c.Next()
    }
}

// checkPermission 检查接口权限
func checkPermission(c *gin.Context, adminUser *model.AdminUser) error {
    enforcer := casbinx.GetEnforcer()
    if enforcer.Error() != nil {
        log.Logger.Error("权限验证初始化失败", zap.Error(enforcer.Error()))
        return e.NewBusinessError(e.ServerErr, "权限验证初始化失败")
    }

    // 构建权限检查的 key
    userKey := fmt.Sprintf("%s%s%d", global.CasbinAdminUserPrefix, global.CasbinSeparator, adminUser.ID)
    path := c.Request.URL.Path
    method := c.Request.Method

    // 检查权限
    ok, err := enforcer.Enforce(userKey, path, method)
    if err != nil {
        log.Logger.Error("权限验证失败", zap.Error(err))
        return e.NewBusinessError(e.ServerErr, "权限验证失败")
    }

    // 如果没有权限,检查接口是否需要授权
    if !ok {
        if model.NewApi().CheckoutRouteIsAuth(path, method) {
            return e.NewBusinessError(e.AuthorizationErr, "暂无接口操作权限")
        }
    }

    return nil
}

权限检查流程

  1. 获取用户信息: 从 JWT Token 中解析用户 ID
  2. 构建用户标识 : 使用 adminUser:1 格式标识用户(1 为用户 ID)
  3. 调用 Enforce : 使用 enforcer.Enforce(userKey, path, method) 检查权限
  4. 处理结果: 如果没有权限且接口需要授权,返回权限不足错误

📊 4. 策略管理

4.1 策略存储格式

在数据库中,策略存储在 casbin_rule 表中,表结构如下:

sql 复制代码
CREATE TABLE `casbin_rule` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `ptype` varchar(100) DEFAULT NULL COMMENT '策略类型:p(策略) 或 g(角色继承)',
  `v0` varchar(100) DEFAULT NULL COMMENT '主体(subject)',
  `v1` varchar(100) DEFAULT NULL COMMENT '对象(object)',
  `v2` varchar(100) DEFAULT NULL COMMENT '操作(action)',
  `v3` varchar(100) DEFAULT NULL,
  `v4` varchar(100) DEFAULT NULL,
  `v5` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_casbin_rule` (`ptype`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

4.2 策略类型

P 策略(权限策略)

格式:[p, subject, object, action]

示例:

  • [p, menu:1, /api/v1/user/list, GET] - 菜单1可以访问 /api/v1/user/list 的 GET 方法
  • [p, role:1, /api/v1/user/*, *] - 角色1可以访问所有 /api/v1/user/* 路径的所有方法
G 策略(角色继承)

格式:[g, user, role]

示例:

  • [g, adminUser:1, role:1] - 用户1继承角色1的所有权限
  • [g, role:1, menu:1] - 角色1继承菜单1的所有权限
  • [g, dept:1, role:1] - 部门1继承角色1的所有权限

4.3 编辑权限策略

项目中提供了 EditPolicyPermissions 方法来编辑权限策略:

go 复制代码
// EditPolicyPermissions 编辑策略权限
// 策略格式:[p, 菜单ID, 接口路径, 接口方法]
func (e *CasbinEnforcer) EditPolicyPermissions(user string, policy [][]string) error {
    return e.WithTransaction(func(enforcer casbin.IEnforcer) error {
        // 1. 删除用户的所有权限
        _, err := enforcer.DeletePermissionsForUser(user)
        if err != nil {
            return err
        }

        if len(policy) == 0 {
            return nil
        }

        // 2. 构建完整的策略规则
        var policies [][]string
        for _, p := range policy {
            if len(p) > 0 {
                // 策略格式:[user, route, method]
                policies = append(policies, append([]string{user}, p...))
            }
        }

        // 3. 批量添加所有策略
        ok, err := enforcer.AddPolicies(policies)
        if err != nil {
            return err
        }

        if !ok {
            return errors.New("添加权限失败")
        }

        return nil
    })
}

4.4 编辑角色继承

项目中提供了 EditPolicyRoles 方法来编辑角色继承关系:

go 复制代码
// EditPolicyRoles 编辑策略角色
// 策略格式:[g, user, role]
func (e *CasbinEnforcer) EditPolicyRoles(user string, policy []string) error {
    return e.WithTransaction(func(enforcer casbin.IEnforcer) error {
        // 1. 删除用户的所有角色
        _, err := enforcer.DeleteRolesForUser(user)
        if err != nil {
            return err
        }

        if len(policy) == 0 {
            return nil
        }

        // 2. 构建完整的角色继承规则
        var rules [][]string
        for _, role := range policy {
            if role != "" {
                // 策略格式:[user, role]
                rules = append(rules, []string{user, role})
            }
        }

        // 3. 批量添加所有角色继承关系
        ok, err := enforcer.AddGroupingPolicies(rules)
        if err != nil {
            return err
        }

        if !ok {
            return errors.New("添加权限失败")
        }

        return nil
    })
}

🏗️ 5. 实际应用场景

5.1 菜单权限管理

在管理后台系统中,菜单通常与 API 接口关联。当用户编辑菜单权限时,需要更新 Casbin 策略:

go 复制代码
// internal/service/permission/menu.go

// UpdateMenuPermissions 更新菜单权限
func (s *MenuService) UpdateMenuPermissions(menu *model.Menu, apiList []uint, tx ...*gorm.DB) error {
    // 1. 查询 API 信息
    apis := model.List(model.NewApi(), "id IN ?", []any{apiList}, model.ListOptionalParams{
        SelectFields: []string{"id", "route", "method"},
    })

    // 2. 构建策略规则
    policy := lo.Map(apis, func(api *model.Api, _ int) []string {
        return []string{api.Route, api.Method}
    })

    // 3. 更新 Casbin 策略
    menuName := fmt.Sprintf("%s%s%d", global.CasbinMenuPrefix, global.CasbinSeparator, menu.ID)
    enforcer := casbinx.GetEnforcer()
    if len(tx) > 0 {
        enforcer.SetDB(tx[0])
    }
    
    return enforcer.EditPolicyPermissions(menuName, policy)
}

5.2 用户角色分配

当为用户分配角色时,需要更新角色继承关系:

go 复制代码
// internal/service/permission/admin_user.go

// EditUserRoles 编辑用户角色
func (s *AdminUserService) EditUserRoles(uid uint, roleIds []uint, tx ...*gorm.DB) error {
    // 1. 构建角色标识列表
    roleList := lo.Map(roleIds, func(roleId uint, _ int) string {
        return fmt.Sprintf("%s%s%d", global.CasbinRolePrefix, global.CasbinSeparator, roleId)
    })

    // 2. 更新角色继承关系
    userName := fmt.Sprintf("%s%s%d", global.CasbinAdminUserPrefix, global.CasbinSeparator, uid)
    enforcer := casbinx.GetEnforcer()
    if len(tx) > 0 {
        enforcer.SetDB(tx[0])
    }
    
    return enforcer.EditPolicyRoles(userName, roleList)
}

5.3 角色权限管理

角色可以继承菜单的权限,也可以继承其他角色的权限:

go 复制代码
// internal/service/permission/role.go

// EditRoleMenus 编辑角色菜单
func (s *RoleService) EditRoleMenus(roleId uint, menuIds []uint, tx ...*gorm.DB) error {
    // 1. 构建菜单标识列表
    menuList := lo.Map(menuIds, func(menuId uint, _ int) string {
        return fmt.Sprintf("%s%s%d", global.CasbinMenuPrefix, global.CasbinSeparator, menuId)
    })

    // 2. 更新角色继承关系
    roleName := fmt.Sprintf("%s%s%d", global.CasbinRolePrefix, global.CasbinSeparator, roleId)
    enforcer := casbinx.GetEnforcer()
    if len(tx) > 0 {
        enforcer.SetDB(tx[0])
    }
    
    return enforcer.EditPolicyRoles(roleName, menuList)
}

🔄 6. 事务支持

在实际应用中,权限更新通常需要与业务数据更新在同一个事务中。项目提供了事务支持:

go 复制代码
// WithTransaction 执行事务中的操作
func (e *CasbinEnforcer) WithTransaction(fc func(e casbin.IEnforcer) error) (err error) {
    a, ok := e.GetAdapter().(*gormadapter.Adapter)
    if !ok {
        return errors.New("适配器类型错误")
    }

    if e.tx != nil {
        if !isInTransaction(e.tx) {
            return errors.New("请先通过 GORM 开启事务后传入 SetDB")
        }

        defer func() {
            // 操作完成后,重置适配器
            e.SetAdapter(a.Copy())
            e.tx = nil
        }()

        // 创建事务适配器
        gormadapter.TurnOffAutoMigrate(e.tx)
        txAdapter, err := gormadapter.NewAdapterByDB(e.tx)
        if err != nil {
            return err
        }
        e.SetAdapter(txAdapter)
    }

    err = fc(e.Enforcer)
    return
}

使用示例:

go 复制代码
// 在 GORM 事务中使用 Casbin
db.Transaction(func(tx *gorm.DB) error {
    // 1. 设置事务
    enforcer := casbinx.GetEnforcer().SetDB(tx)

    // 2. 更新业务数据
    // ... 业务逻辑 ...

    // 3. 更新权限策略(在事务中)
    menuName := fmt.Sprintf("menu:%d", menuId)
    return enforcer.EditPolicyPermissions(menuName, policy)
})

📝 7. 策略重新加载

当策略更新后,需要重新加载策略到内存中:

go 复制代码
// 重新加载策略
enforcer := casbinx.GetEnforcer()
if err := enforcer.LoadPolicy(); err != nil {
    log.Logger.Error("重新加载策略失败", zap.Error(err))
}

注意 :虽然启用了 EnableAutoSave(true),但这只保证策略保存到数据库,不会自动加载到内存。在策略更新后,需要手动调用 LoadPolicy() 重新加载。

🎯 8. 权限标识规范

项目中定义了统一的权限标识前缀:

go 复制代码
// internal/global/auth.go

const (
    CasbinAdminUserPrefix = "adminUser"  // 用户前缀
    CasbinRolePrefix     = "role"        // 角色前缀
    CasbinMenuPrefix     = "menu"        // 菜单前缀
    CasbinDeptPrefix     = "dept"        // 部门前缀
    CasbinSeparator      = ":"           // 分隔符
)

权限标识格式:

  • 用户:adminUser:1(用户 ID 为 1)
  • 角色:role:1(角色 ID 为 1)
  • 菜单:menu:1(菜单 ID 为 1)
  • 部门:dept:1(部门 ID 为 1)

🚨 9. 常见问题与解决方案

9.1 权限更新后不生效

问题:更新策略后,权限检查仍然使用旧策略。

解决方案 :在策略更新后,调用 LoadPolicy() 重新加载策略:

go 复制代码
enforcer := casbinx.GetEnforcer()
// 更新策略
enforcer.EditPolicyPermissions(userName, policy)
// 重新加载策略
enforcer.LoadPolicy()

9.2 事务回滚后策略未恢复

问题:在事务中更新策略,事务回滚后策略未恢复。

解决方案:确保在事务提交或回滚后,重新加载策略:

go 复制代码
db.Transaction(func(tx *gorm.DB) error {
    enforcer := casbinx.GetEnforcer().SetDB(tx)
    // 更新策略
    return enforcer.EditPolicyPermissions(userName, policy)
})

// 事务提交或回滚后,重新加载策略
enforcer := casbinx.GetEnforcer()
enforcer.LoadPolicy()

9.3 路径匹配问题

问题 :使用 keyMatch3 函数时,路径匹配不符合预期。

解决方案

  • keyMatch3 支持 * 通配符,如 /api/v1/user/* 可以匹配 /api/v1/user/list/api/v1/user/detail
  • 如果需要更精确的匹配,可以使用 keyMatchregexMatch

📚 10. 最佳实践

  1. 统一权限标识格式:使用统一的前缀和分隔符,便于管理和维护
  2. 事务一致性:权限更新与业务数据更新应在同一事务中
  3. 策略重新加载:策略更新后及时重新加载,确保权限检查使用最新策略
  4. 错误处理:完善的错误处理和日志记录,便于问题排查
  5. 性能优化:策略加载到内存后,权限检查速度很快,但策略更新后需要重新加载

🎉 总结

本文详细介绍了如何在 Go 项目中使用 Casbin 实现 RBAC 权限管理,包括:

  • ✅ Casbin 模型配置
  • ✅ Enforcer 初始化
  • ✅ 权限检查中间件
  • ✅ 策略管理(权限策略和角色继承)
  • ✅ 事务支持
  • ✅ 实际应用场景
  • ✅ 常见问题与解决方案

基于本文的实现,你可以快速构建一个功能完整的权限管理系统。如果你需要完整的项目代码,可以参考开源项目 gin-layout,这是一个功能完整的 Go Admin 后台管理系统框架,提供了开箱即用的权限管理解决方案。

🔗 相关资源


如果这篇文章对你有帮助,欢迎 Star ⭐ 项目 gin-layout 支持一下!

相关推荐
源代码•宸1 小时前
GoLang写一个火星漫游行动
开发语言·经验分享·后端·golang
i***13241 小时前
【SpringBoot】单元测试实战演示及心得分享
spring boot·后端·单元测试
小尧嵌入式1 小时前
C++中的封装继承多态
开发语言·arm开发·c++
csbysj20201 小时前
Redis 配置详解
开发语言
s***35301 小时前
SpringMVC新版本踩坑[已解决]
android·前端·后端
行走在电子领域的工匠1 小时前
台达ST:自定义串行通讯传送与接收指令COMRS程序范例四
开发语言·台达plc·st语言编程
t198751281 小时前
基于因子图与和积算法的MATLAB实现
开发语言·算法·matlab
霸王大陆1 小时前
《零基础学 PHP:从入门到实战》教程-模块四:数组与函数-1
android·开发语言·php
le serein —f1 小时前
用go实现-回文链表
算法·leetcode·golang