gin casbin RBAC 设计权限系统表设计

1,167 阅读8分钟

个人笔记而已-不要花时间看哟~

提问01:gin casbin RBAC 设计权限系统表及关联关系,并给出备注注释

Gin + Casbin RBAC 权限系统设计

下面是一个基于Gin框架和Casbin的RBAC(基于角色的访问控制)权限系统的数据库表设计和关联关系说明。

数据库表设计

1. 用户表 (users)

CREATE TABLE `users` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `password` varchar(100) NOT NULL COMMENT '密码(加密存储)',
  `email` varchar(100) DEFAULT NULL COMMENT '邮箱',
  `phone` varchar(20) DEFAULT NULL COMMENT '手机号',
  `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态(0:禁用,1:启用)',
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_username` (`username`),
  UNIQUE KEY `idx_email` (`email`),
  KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

2. 角色表 (roles)

CREATE TABLE `roles` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色ID',
  `name` varchar(50) NOT NULL COMMENT '角色名称',
  `description` varchar(255) DEFAULT NULL COMMENT '角色描述',
  `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态(0:禁用,1:启用)',
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_name` (`name`),
  KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';

3. 用户角色关联表 (user_roles)

CREATE TABLE `user_roles` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '关联ID',
  `user_id` bigint NOT NULL COMMENT '用户ID',
  `role_id` bigint NOT NULL COMMENT '角色ID',
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_user_role` (`user_id`,`role_id`),
  KEY `idx_role_id` (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表';

4. 资源表 (resources)

CREATE TABLE `resources` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '资源ID',
  `name` varchar(50) NOT NULL COMMENT '资源名称',
  `code` varchar(100) NOT NULL COMMENT '资源代码(唯一标识)',
  `type` tinyint NOT NULL COMMENT '资源类型(1:菜单,2:按钮,3:API)',
  `parent_id` bigint DEFAULT NULL COMMENT '父资源ID',
  `path` varchar(255) DEFAULT NULL COMMENT '资源路径(前端路由或API路径)',
  `method` varchar(10) DEFAULT NULL COMMENT 'HTTP方法(GET,POST等,API类型时使用)',
  `icon` varchar(50) DEFAULT NULL COMMENT '图标(菜单类型时使用)',
  `sort` int DEFAULT '0' COMMENT '排序',
  `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态(0:禁用,1:启用)',
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_code` (`code`),
  KEY `idx_parent_id` (`parent_id`),
  KEY `idx_type` (`type`),
  KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='资源表(菜单/按钮/API)';

5. 角色资源关联表 (role_resources)

CREATE TABLE `role_resources` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '关联ID',
  `role_id` bigint NOT NULL COMMENT '角色ID',
  `resource_id` bigint NOT NULL COMMENT '资源ID',
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_role_resource` (`role_id`,`resource_id`),
  KEY `idx_resource_id` (`resource_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色资源关联表';

6. Casbin规则表 (casbin_rule)

CREATE TABLE `casbin_rule` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '规则ID',
  `ptype` varchar(10) NOT NULL COMMENT '策略类型(p:策略,g:角色继承)',
  `v0` varchar(100) NOT NULL COMMENT '角色/用户(subject)',
  `v1` varchar(100) NOT NULL COMMENT '资源/路径(object)',
  `v2` varchar(100) DEFAULT NULL COMMENT '操作/方法(action)',
  `v3` varchar(100) DEFAULT NULL COMMENT '预留字段1',
  `v4` varchar(100) DEFAULT NULL COMMENT '预留字段2',
  `v5` varchar(100) DEFAULT NULL COMMENT '预留字段3',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_unique` (`ptype`,`v0`,`v1`,`v2`,`v3`,`v4`,`v5`),
  KEY `idx_v0` (`v0`),
  KEY `idx_v1` (`v1`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Casbin规则表';

表关系说明

  1. 用户与角色:多对多关系,通过user_roles表关联

    • 一个用户可以拥有多个角色
    • 一个角色可以分配给多个用户
  2. 角色与资源:多对多关系,通过role_resources表关联

    • 一个角色可以访问多个资源
    • 一个资源可以被多个角色访问
  3. 资源自身:树形结构,通过parent_id自关联

    • 支持多级菜单/资源结构
  4. Casbin规则表:存储实际的访问控制规则

    • ptype字段区分策略类型(p表示策略规则,g表示角色继承关系)
    • 通常格式:
      • 策略规则: p, role, resource, action (角色可以访问资源的某个操作)
      • 角色继承: g, user, role (用户属于某个角色)

RBAC模型设计

Casbin使用的RBAC模型通常如下(policy.csv):

p, admin, /api/v1/users, GET
p, admin, /api/v1/users, POST
p, editor, /api/v1/articles, GET
p, editor, /api/v1/articles, POST
g, alice, admin
g, bob, editor

Gin + Casbin集成建议

  1. 初始化Casbin
import (
    "github.com/casbin/casbin/v2"
    gormadapter "github.com/casbin/gorm-adapter/v3"
    "gorm.io/gorm"
)

func SetupCasbin(db *gorm.DB) *casbin.Enforcer {
    adapter, err := gormadapter.NewAdapterByDB(db)
    if err != nil {
        panic(err)
    }
    
    enforcer, err := casbin.NewEnforcer("path/to/rbac_model.conf", adapter)
    if err != nil {
        panic(err)
    }
    
    // 加载策略
    err = enforcer.LoadPolicy()
    if err != nil {
        panic(err)
    }
    
    return enforcer
}
  1. 中间件检查权限
func CasbinMiddleware(enforcer *casbin.Enforcer) gin.HandlerFunc {
    return func(c *gin.Context) {
        // 获取当前用户角色
        user, exists := c.Get("user")
        if !exists {
            c.AbortWithStatusJSON(401, gin.H{"error": "unauthorized"})
            return
        }
        
        // 获取请求路径和方法
        path := c.Request.URL.Path
        method := c.Request.Method
        
        // 检查权限
        ok, err := enforcer.Enforce(user.(string), path, method)
        if err != nil {
            c.AbortWithStatusJSON(500, gin.H{"error": "internal server error"})
            return
        }
        
        if !ok {
            c.AbortWithStatusJSON(403, gin.H{"error": "forbidden"})
            return
        }
        
        c.Next()
    }
}
  1. RBAC模型文件(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) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*")

数据初始化建议

  1. 初始化超级管理员角色
  2. 初始化基本资源(如首页、登录页等)
  3. 为超级管理员分配所有资源权限

这种设计提供了灵活的RBAC权限控制,可以满足大多数Web应用的权限管理需求。

提问02: 需要菜单表吗?如何跟上面的设计完美融合

菜单表设计与RBAC系统融合

在您的设计中,实际上已经包含了菜单功能 - 资源表(resources)中的type字段已经区分了资源类型(1:菜单,2:按钮,3:API)。这种设计是合理的,但我可以提供更明确的菜单表设计方案,并说明如何与现有RBAC系统完美融合。

方案一:使用现有资源表作为菜单表(推荐)

这是最简洁的方案,利用现有的resources表:

ALTER TABLE `resources` 
MODIFY COLUMN `type` tinyint NOT NULL COMMENT '资源类型(1:菜单,2:按钮,3:API)',
ADD COLUMN `component` varchar(100) DEFAULT NULL COMMENT '前端组件路径(菜单类型时使用)',
ADD COLUMN `visible` tinyint DEFAULT 1 COMMENT '是否显示(0:隐藏,1:显示)',
ADD COLUMN `is_frame` tinyint DEFAULT 0 COMMENT '是否外链(0:否,1:是)';

优点

  • 保持表结构简单
  • 菜单、按钮、API统一管理
  • 权限控制一致

查询用户菜单示例

SELECT r.* FROM resources r
JOIN role_resources rr ON r.id = rr.resource_id
JOIN user_roles ur ON rr.role_id = ur.role_id
WHERE ur.user_id = ? AND r.type = 1 AND r.status = 1
ORDER BY r.sort;

方案二:单独菜单表设计

如果您希望更明确地区分菜单和权限,可以单独设计菜单表:

1. 菜单表 (menus)

CREATE TABLE `menus` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
  `name` varchar(50) NOT NULL COMMENT '菜单名称',
  `code` varchar(100) NOT NULL COMMENT '菜单编码(唯一)',
  `parent_id` bigint DEFAULT NULL COMMENT '父菜单ID',
  `path` varchar(255) DEFAULT NULL COMMENT '路由路径',
  `component` varchar(100) DEFAULT NULL COMMENT '组件路径',
  `icon` varchar(50) DEFAULT NULL COMMENT '图标',
  `sort` int DEFAULT '0' COMMENT '排序',
  `visible` tinyint DEFAULT 1 COMMENT '是否显示(0:隐藏,1:显示)',
  `is_frame` tinyint DEFAULT 0 COMMENT '是否外链(0:否,1:是)',
  `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态(0:禁用,1:启用)',
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_code` (`code`),
  KEY `idx_parent_id` (`parent_id`),
  KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜单表';

2. 菜单资源关联表 (menu_resources)

CREATE TABLE `menu_resources` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '关联ID',
  `menu_id` bigint NOT NULL COMMENT '菜单ID',
  `resource_id` bigint NOT NULL COMMENT '资源ID',
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_menu_resource` (`menu_id`,`resource_id`),
  KEY `idx_resource_id` (`resource_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜单资源关联表';

融合方式

  1. 菜单表只存储展示相关的信息
  2. 资源表存储权限控制相关的信息
  3. 通过关联表建立菜单与资源的联系

优点

  • 菜单与权限解耦
  • 更清晰的职责划分
  • 一个菜单可以关联多个资源(权限)

缺点

  • 表结构更复杂
  • 需要维护更多关联关系

完美融合的关键点

无论选择哪种方案,要实现完美融合需要注意:

  1. 数据一致性

    • 当菜单/资源状态变为禁用时,要同步更新相关缓存
    • 删除菜单时要处理关联的资源权限
  2. 缓存策略

    // 获取用户菜单(带缓存)
    func GetUserMenus(userID int64) ([]Menu, error) {
        cacheKey := fmt.Sprintf("user_menus:%d", userID)
        if cached, err := cache.Get(cacheKey); err == nil {
            return cached.([]Menu), nil
        }
        
        // 查询数据库
        menus := queryMenusFromDB(userID)
        
        // 设置缓存
        cache.Set(cacheKey, menus, 30*time.Minute)
        return menus, nil
    }
    
  3. 前端对接

    • 返回的菜单结构应该包含完整的路由信息和权限标识
    • 示例返回结构:
      {
        "path": "/system",
        "component": "Layout",
        "children": [
          {
            "path": "user",
            "component": "system/user/index",
            "meta": {
              "title": "用户管理",
              "permissions": ["system:user:query"]
            }
          }
        ]
      }
      
  4. 动态路由注册

    // 前端动态路由示例(Vue)
    const permission = {
      state: {
        routes: [],
        addRoutes: []
      },
      actions: {
        generateRoutes({ commit }, roles) {
          return new Promise(resolve => {
            // 通过API获取用户菜单
            getMenus().then(res => {
              const accessedRoutes = filterAsyncRoutes(res.data)
              commit('SET_ROUTES', accessedRoutes)
              resolve(accessedRoutes)
            })
          })
        }
      }
    }
    

推荐方案

对于大多数项目,我推荐方案一(使用现有资源表作为菜单表),因为:

  1. 菜单本质上也是一种资源,与权限控制紧密相关
  2. 减少了表关联和复杂度
  3. 更易于维护数据一致性
  4. 符合KISS(Keep It Simple, Stupid)原则

只有在菜单系统特别复杂,且与权限系统有明显不同生命周期时,才考虑方案二。