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
}
关键点说明
- 单例模式 : 使用
sync.Once确保 Enforcer 只初始化一次 - GORM 适配器 : 使用
gorm-adapter将策略存储在 MySQL 数据库中 - 自动保存 :
EnableAutoSave(true)确保策略变更后自动保存到数据库 - 模型加载: 从配置文件加载访问控制模型
🔐 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
}
权限检查流程
- 获取用户信息: 从 JWT Token 中解析用户 ID
- 构建用户标识 : 使用
adminUser:1格式标识用户(1 为用户 ID) - 调用 Enforce : 使用
enforcer.Enforce(userKey, path, method)检查权限 - 处理结果: 如果没有权限且接口需要授权,返回权限不足错误
📊 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等- 如果需要更精确的匹配,可以使用
keyMatch或regexMatch
📚 10. 最佳实践
- 统一权限标识格式:使用统一的前缀和分隔符,便于管理和维护
- 事务一致性:权限更新与业务数据更新应在同一事务中
- 策略重新加载:策略更新后及时重新加载,确保权限检查使用最新策略
- 错误处理:完善的错误处理和日志记录,便于问题排查
- 性能优化:策略加载到内存后,权限检查速度很快,但策略更新后需要重新加载
🎉 总结
本文详细介绍了如何在 Go 项目中使用 Casbin 实现 RBAC 权限管理,包括:
- ✅ Casbin 模型配置
- ✅ Enforcer 初始化
- ✅ 权限检查中间件
- ✅ 策略管理(权限策略和角色继承)
- ✅ 事务支持
- ✅ 实际应用场景
- ✅ 常见问题与解决方案
基于本文的实现,你可以快速构建一个功能完整的权限管理系统。如果你需要完整的项目代码,可以参考开源项目 gin-layout,这是一个功能完整的 Go Admin 后台管理系统框架,提供了开箱即用的权限管理解决方案。
🔗 相关资源
如果这篇文章对你有帮助,欢迎 Star ⭐ 项目 gin-layout 支持一下!