跳到主要内容在 Go 管理后台里落地 Casbin RBAC | 极客日志Go / Golang
在 Go 管理后台里落地 Casbin RBAC
这篇内容围绕 Go 管理后台里的 Casbin RBAC 落地展开:先用模型文件定义请求、策略、角色继承和匹配规则,再通过 gorm-adapter 把策略存到 MySQL。中间件层在 Gin 中完成登录校验和权限判断,管理员、角色、菜单、部门都用统一前缀标识。文章还说明了如何在事务中更新权限、为什么策略改完后要手动 LoadPolicy,以及路径匹配和回滚状态这几个常见坑。
活在当下54 浏览 在 Go 管理后台里落地 Casbin RBAC
做企业级 Go 管理后台,权限管理基本绕不开。Casbin 适合这类场景的地方,不是'模型多',而是它把权限模型和策略存储拆开了:模型管规则,适配器管数据。这样一来,RBAC 只是起点,后面想接 ACL、ABAC 或者 RESTful 路由匹配,也不用推翻重来。
Casbin 是怎么工作的
Casbin 支持几种常见的访问控制模型:
- ACL (Access Control List) - 访问控制列表
- RBAC (Role-Based Access Control) - 基于角色的访问控制
- ABAC (Attribute-Based Access Control) - 基于属性的访问控制
- RESTful - RESTful 风格的访问控制
核心思路很简单:模型文件定义'怎么判定有权限',策略表里存'谁对什么有权限'。这两个东西分开,后面改权限规则时不会把业务代码搅乱。
依赖安装
go get github.com/casbin/casbin/v2 github.com/casbin/gorm-adapter/v3
模型配置
项目里用的是 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))
这份配置里,r 是请求,p 是策略,g 负责角色继承。真正起作用的是 matchers:
g(r.sub, p.sub):先看用户是不是属于某个角色链路
keyMatch3(r.obj, p.obj):路径按规则匹配,* 这类通配符能用
regexMatch(r.act, p.act):方法名按正则匹配,GET、POST 之类都能兜住
Enforcer 初始化
项目把 Casbin 包了一层,放在 internal/pkg/utils/casbin/casbin.go。思路不复杂:模型从文件读,策略落到 MySQL,Enforcer 只初始化一次。
package casbinx
import (
"fmt"
"sync"
"github.com/casbin/casbin/v2"
"github.com/casbin/casbin/v2/model"
gormadapter "github.com/casbin/gorm-adapter/v3"
)
CasbinEnforcer {
*casbin.Enforcer
errInit
tx *gorm.DB
model model.Model
}
casbinx = &CasbinEnforcer{}
{
once.Do( {
modelPath, err := getModelPath()
err != {
casbinx.errInit = err
}
m, err := model.NewModelFromFile(modelPath)
err != {
casbinx.errInit = fmt.Errorf(, err)
}
casbinx.model = m
db := data.MysqlDB()
gormadapter.TurnOffAutoMigrate(db)
adapter, err := gormadapter.NewAdapterByDB(db)
err != {
casbinx.errInit = fmt.Errorf(, err)
}
enforcer, err := casbin.NewEnforcer(m, adapter)
err != {
casbinx.errInit = fmt.Errorf(, err)
}
enforcer.EnableAutoSave()
casbinx.Enforcer = enforcer
})
casbinx.errInit
}
*CasbinEnforcer {
casbinx.Enforcer == {
err := InitEnforcer(); err != {
}
}
casbinx
}
"gorm.io/gorm"
type
struct
error
var
func InitEnforcer()
error
func()
if
nil
return
if
nil
"加载模型失败:%w"
return
if
nil
"创建适配器失败:%w"
return
if
nil
"创建 Enforcer 失败:%w"
return
true
return
func GetEnforcer()
if
nil
if
nil
return
nil
return
sync.Once 用来保证初始化只发生一次,不然线上很容易撞重复创建。
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
}
这段逻辑里,真正要盯住的是两件事:用户身份是不是完整,权限判断是不是落到 Casbin 上。超级管理员直接放行,这种处理很常见,也省掉不少策略噪音。
策略是怎么存的
用 gorm-adapter 后,策略会进 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;
ptype 区分策略类型,p 是权限本身,g 是继承关系。Casbin 的好处就在这里:它不会强迫你把'角色'和'资源'揉成一个难维护的表结构。
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 的所有权限
编辑权限和角色继承
项目里把'改权限'和'改角色'拆成了两个方法,处理方式都比较直接:先删旧的,再批量加新的。这个做法不花哨,但对后台管理足够稳。
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
})
}
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)
}
事务里处理权限更新
权限更新最好跟业务数据放在同一个事务里,不然很容易出现'数据改了,权限没改'或者反过来的脏状态。这里的实现是把 Casbin 的 adapter 切到当前事务上,做完再恢复。
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)
})
策略更新后要重新加载
EnableAutoSave(true) 只负责写库,不会自动把内存里的策略刷新掉。这个坑挺常见,尤其是你刚改完权限就马上测,很容易以为'没生效'。
enforcer := casbinx.GetEnforcer()
if err := enforcer.LoadPolicy(); err != nil {
log.Logger.Error("重新加载策略失败", zap.Error(err))
}
统一权限标识
项目里把用户、角色、菜单、部门都约定成统一前缀,读起来会清楚很多,也方便后面批量处理。
const (
CasbinAdminUserPrefix = "adminUser"
CasbinRolePrefix = "role"
CasbinMenuPrefix = "menu"
CasbinDeptPrefix = "dept"
CasbinSeparator = ":"
)
- 用户:
adminUser:1
- 角色:
role:1
- 菜单:
menu:1
- 部门:
dept:1
常见问题
更新后权限没变
一般是策略写进库了,但内存里的 Enforcer 还在用旧数据。处理方式就是在更新后手动 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 不是万能的,但对大多数后台路由够用。/api/v1/user/* 可以覆盖列表、详情这类接口;如果你的路径规则更细,就换成 keyMatch 或 regexMatch。
我会怎么用这套方案
这套实现最实用的地方,不是 Casbin 本身,而是它把后台里最烦的那部分收住了:用户、角色、菜单、接口之间的关系,不需要靠一堆手写 if/else 拼起来。权限更新走事务,策略存库,查询走内存,整体已经比较顺手。
当然,它也不是拿来就完美。真正要留心的是策略刷新和事务边界,别让'改了但没生效'这种问题混进去。把这两点处理好,RBAC 基本就能在 Go 管理后台里稳定跑起来。
相关免费在线工具
- 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