跳到主要内容Go 项目中使用 Casbin 实现 RBAC 权限管理 | 极客日志Go / Golang
Go 项目中使用 Casbin 实现 RBAC 权限管理
Go 项目集成 Casbin 库实现 RBAC 权限控制。通过配置访问控制模型定义规则,利用 GORM 适配器将策略存储于 MySQL。核心流程包括 Enforcer 初始化、中间件权限校验、策略增删改查及事务支持。涵盖用户角色分配、菜单权限管理及路径匹配优化等场景,确保权限检查高效准确。
活在当下2 浏览 Go 项目中使用 Casbin 实现 RBAC 权限管理
前言
在构建企业级 Go 管理后台系统时,权限管理是一个核心功能。Casbin 是一个强大的、开源的访问控制库,支持多种访问控制模型(ACL、RBAC、ABAC 等)。本文将详细介绍如何在 Go 项目中使用 Casbin 实现完整的 RBAC(基于角色的访问控制)权限管理系统。
什么是 Casbin?
Casbin 是一个强大的、开源的访问控制库,支持多种访问控制模型:
- ACL (Access Control List) - 访问控制列表
- RBAC (Role-Based Access Control) - 基于角色的访问控制
- ABAC (Attribute-Based Access Control) - 基于属性的访问控制
- RESTful - RESTful 风格的访问控制
Casbin 的核心思想是将访问控制模型与策略分离,通过配置文件定义访问控制模型,通过适配器(Adapter)存储策略规则。
项目依赖
首先,我们需要安装 Casbin 相关的依赖包:
go get github.com/casbin/casbin/v2 github.com/casbin/gorm-adapter/v3
Casbin 模型配置
Casbin 使用模型文件(Model)来定义访问控制规则。在项目中,我们创建了 rbac_model.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 方法
Casbin 初始化
在项目中,我们封装了一个 Casbin 工具包 internal/pkg/utils/casbin/casbin.go:
package casbinx
import (
"fmt"
"sync"
"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{}
func InitEnforcer() error {
once.Do(func() {
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
db := data.MysqlDB()
gormadapter.TurnOffAutoMigrate(db)
adapter, err := gormadapter.NewAdapterByDB(db)
if err != nil {
casbinx.errInit = fmt.Errorf("创建适配器失败:%w", err)
return
}
enforcer, err := casbin.NewEnforcer(m, adapter)
if err != nil {
casbinx.errInit = fmt.Errorf("创建 Enforcer 失败:%w", err)
return
}
enforcer.EnableAutoSave(true)
casbinx.Enforcer = enforcer
})
return casbinx.errInit
}
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) 确保策略变更后自动保存到数据库
- 模型加载: 从配置文件加载访问控制模型
权限检查中间件
在 Gin 框架中,我们通过中间件来实现权限检查:
func AdminAuthHandler() gin.HandlerFunc {
return func(c *gin.Context) {
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
}
if !isSuperAdmin(adminUser) {
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()
}
}
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, "权限验证初始化失败")
}
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) 检查权限
- 处理结果: 如果没有权限且接口需要授权,返回权限不足错误
策略管理
策略存储格式
在数据库中,策略存储在 casbin_rule 表中,表结构如下:
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=utf8mb3;
策略类型
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, adminUser:1, role:1] - 用户 1 继承角色 1 的所有权限
[g, role:1, menu:1] - 角色 1 继承菜单 1 的所有权限
[g, dept:1, role:1] - 部门 1 继承角色 1 的所有权限
编辑权限策略
项目中提供了 EditPolicyPermissions 方法来编辑权限策略:
func (e *CasbinEnforcer) EditPolicyPermissions(user string, policy [][]string) error {
return e.WithTransaction(func(enforcer casbin.IEnforcer) error {
_, err := enforcer.DeletePermissionsForUser(user)
if err != nil {
return err
}
if len(policy) == 0 {
return nil
}
var policies [][]string
for _, p := range policy {
if len(p) > 0 {
policies = append(policies, append([]string{user}, p...))
}
}
ok, err := enforcer.AddPolicies(policies)
if err != nil {
return err
}
if !ok {
return errors.New("添加权限失败")
}
return nil
})
}
编辑角色继承
项目中提供了 EditPolicyRoles 方法来编辑角色继承关系:
func (e *CasbinEnforcer) EditPolicyRoles(user string, policy []string) error {
return e.WithTransaction(func(enforcer casbin.IEnforcer) error {
_, err := enforcer.DeleteRolesForUser(user)
if err != nil {
return err
}
if len(policy) == 0 {
return nil
}
var rules [][]string
for _, role := range policy {
if role != "" {
rules = append(rules, []string{user, role})
}
}
ok, err := enforcer.AddGroupingPolicies(rules)
if err != nil {
return err
}
if !ok {
return errors.New("添加权限失败")
}
return nil
})
}
实际应用场景
菜单权限管理
在管理后台系统中,菜单通常与 API 接口关联。当用户编辑菜单权限时,需要更新 Casbin 策略:
func (s *MenuService) UpdateMenuPermissions(menu *model.Menu, apiList []uint, tx ...*gorm.DB) error {
apis := model.List(model.NewApi(), "id IN ?", []any{apiList}, model.ListOptionalParams{
SelectFields: []string{"id", "route", "method"},
})
policy := lo.Map(apis, func(api *model.Api, _ int) []string {
return []string{api.Route, api.Method}
})
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)
}
用户角色分配
func (s *AdminUserService) EditUserRoles(uid uint, roleIds []uint, tx ...*gorm.DB) error {
roleList := lo.Map(roleIds, func(roleId uint, _ int) string {
return fmt.Sprintf("%s%s%d", global.CasbinRolePrefix, global.CasbinSeparator, roleId)
})
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)
}
角色权限管理
角色可以继承菜单的权限,也可以继承其他角色的权限:
func (s *RoleService) EditRoleMenus(roleId uint, menuIds []uint, tx ...*gorm.DB) error {
menuList := lo.Map(menuIds, func(menuId uint, _ int) string {
return fmt.Sprintf("%s%s%d", global.CasbinMenuPrefix, global.CasbinSeparator, menuId)
})
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)
}
事务支持
在实际应用中,权限更新通常需要与业务数据更新在同一个事务中。项目提供了事务支持:
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
}
db.Transaction(func(tx *gorm.DB) error {
enforcer := casbinx.GetEnforcer().SetDB(tx)
menuName := fmt.Sprintf("menu:%d", menuId)
return enforcer.EditPolicyPermissions(menuName, policy)
})
策略重新加载
enforcer := casbinx.GetEnforcer()
if err := enforcer.LoadPolicy(); err != nil {
log.Logger.Error("重新加载策略失败", zap.Error(err))
}
注意:虽然启用了 EnableAutoSave(true),但这只保证策略保存到数据库,不会自动加载到内存。在策略更新后,需要手动调用 LoadPolicy() 重新加载。
权限标识规范
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)
常见问题与解决方案
权限更新后不生效
解决方案:在策略更新后,调用 LoadPolicy() 重新加载策略:
enforcer := casbinx.GetEnforcer()
enforcer.EditPolicyPermissions(userName, policy)
enforcer.LoadPolicy()
事务回滚后策略未恢复
db.Transaction(func(tx *gorm.DB) error {
enforcer := casbinx.GetEnforcer().SetDB(tx)
return enforcer.EditPolicyPermissions(userName, policy)
})
enforcer := casbinx.GetEnforcer()
enforcer.LoadPolicy()
路径匹配问题
问题:使用 keyMatch3 函数时,路径匹配不符合预期。
keyMatch3 支持 * 通配符,如 /api/v1/user/* 可以匹配 /api/v1/user/list、/api/v1/user/detail 等
- 如果需要更精确的匹配,可以使用
keyMatch 或 regexMatch
最佳实践
- 统一权限标识格式:使用统一的前缀和分隔符,便于管理和维护
- 事务一致性:权限更新与业务数据更新应在同一事务中
- 策略重新加载:策略更新后及时重新加载,确保权限检查使用最新策略
- 错误处理:完善的错误处理和日志记录,便于问题排查
- 性能优化:策略加载到内存后,权限检查速度很快,但策略更新后需要重新加载
总结
本文详细介绍了如何在 Go 项目中使用 Casbin 实现 RBAC 权限管理,包括:
- Casbin 模型配置
- Enforcer 初始化
- 权限检查中间件
- 策略管理(权限策略和角色继承)
- 事务支持
- 实际应用场景
- 常见问题与解决方案
基于本文的实现,你可以快速构建一个功能完整的权限管理系统。
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online