Skip to content

RBAC权限

采用基于角色的访问控制(RBAC)模型,通过用户-角色-菜单三层关系实现细粒度的权限管理。权限控制贯穿前后端:后端通过 @permission_required 装饰器校验(src/core/access_decorators.py),前端通过 v-perm 指令控制按钮显隐。

权限模型

用户(User) ──N:N── 角色(Role) ──N:N── 菜单(Menu)

 type=0: 菜单(页面路由)
 type=1: 节点(权限按钮)

数据库表关系:

关联表说明
fastapi_user_role用户-角色关联表
fastapi_role_menu角色-菜单关联表
fastapi_menu菜单表(含权限节点)

权限节点格式

权限节点采用 sys:module:action 三段式命名:

sys:user:list 用户列表
sys:user:add 用户新增
sys:user:update 用户编辑
sys:user:delete 用户删除
sys:user:resetPwd 重置密码
sys:role:list 角色列表
sys:role:add 角色新增
sys:menu:list 菜单列表
sys:dept:list 部门列表
sys:level:list 职级列表
sys:position:list 岗位列表

菜单模型

python
# src/modules/system/menu/models.py
# ============================================================
# 菜单模型
# ============================================================
class Menu(base_model, base_db):
    """菜单模型类"""
    # ============================================================
    # 表名配置
    # ============================================================
    __tablename__ = DB_PREFIX + "menu"
    __table_comment__ = "菜单表"

    # ============================================================
    # 字段定义
    # ============================================================
    # 菜单名称
    name = Column(String(150), nullable=False, index=True, comment="菜单名称")
    # 菜单图标
    icon = Column(String(50), nullable=True, comment="菜单图标")
    # 菜单图标(备用)
    icon2 = Column(String(50), nullable=True, comment="菜单图标")
    # 上级ID
    parent_id = Column(Integer, default=0, server_default=text('0'), index=True, comment="上级ID")
    # 菜单路径
    path = Column(String(255), nullable=True, comment="菜单路径")
    # 菜单组件
    component = Column(String(255), nullable=True, comment="菜单组件")
    # 权限节点
    permission = Column(String(150), nullable=True, comment="权限节点")
    # 打开方式:0-组件 1-内链 2-外链
    target = Column(Integer, default=1, server_default=text('1'), comment="打开方式:0-组件 1-内链 2-外链")
    # 菜单类型:0-菜单 1-节点
    type = Column(Integer, default=0, server_default=text('0'), index=True, comment="菜单类型:0-菜单 1-节点")
    # 状态选项
    status = Column(Integer, default=0, server_default=text('0'), index=True, comment="菜单状态:0-正常 1-停用")
    # 是否可见:0-可见 1-不可见
    hide = Column(Integer, default=1, server_default=text('1'), comment="是否可见:0-可见 1-不可见")
    # 排序
    sort = Column(Integer, default=0, server_default=text('0'), comment="排序")
    # 备注
    note = Column(String(255), nullable=True, comment="备注")

    # ============================================================
    # 内置方法
    # ============================================================
    def __str__(self):
        """返回菜单ID作为字符串表示"""
        return "菜单{}".format(self.id)

菜单类型说明:

type说明用途
0菜单页面路由,前端生成菜单导航
1节点权限按钮,控制操作权限

后端权限校验

permission_required 装饰器

src/core/access_decorators.py 中的 @permission_required 装饰器用于端点级权限校验:

python
from core.access_decorators import permission_required, check_demo

@router.post('/add')
@permission_required("sys:level:add")
@check_demo
async def add(request: Request, data: LevelForm):
    return await level.LevelAdd(request, data)

校验逻辑(完整实现):

python
def permission_required(permission: str):
    """节点权限鉴权装饰器,userId==1(管理员)直接放行"""
    def decorator(func):
        def _check(request):
            """同步权限校验(直查DB,无Redis依赖,供线程池/回退场景使用)"""
            userId = get_user_id(request)
            if userId == 1:
                return None
            from modules.system.menu import service as menu
            permission_list = menu.get_permissions_list_sync(userId)
            if permission not in permission_list:
                return R.failed("权限不足")
            return None

        async def _check_cached(request):
            """异步权限校验(走 Redis 缓存,未命中才回源DB)"""
            userId = get_user_id(request)
            if userId == 1:
                return None
            from modules.system.menu import service as menu
            permission_list = await menu.get_permissions_list(userId)
            if permission not in permission_list:
                return R.failed("权限不足")
            return None

        if asyncio.iscoroutinefunction(func):
            @wraps(func)
            async def async_wrapper(*args, **kwargs):
                request = _extract_request(*args, **kwargs)
                denied = await _check_cached(request)
                if denied:
                    return denied
                return await func(*args, **kwargs)
            return async_wrapper

        @wraps(func)
        def sync_wrapper(*args, **kwargs):
            request = _extract_request(*args, **kwargs)
            denied = _check(request)
            if denied:
                return denied
            return func(*args, **kwargs)
        return sync_wrapper
    return decorator

Request 提取

装饰器通过 _extract_request() 从函数参数中提取 Request 对象,支持 request 关键字参数或位置参数两种方式。如果端点函数没有 request 参数,权限校验将被跳过。

管理员免检

用户ID为1的管理员账号(admin)自动跳过所有权限校验,拥有全部权限。

同步/异步适配

装饰器自动适配同步和异步端点:

  • 异步端点:走 Redis 缓存的 get_permissions_list(),性能更优
  • 同步端点:走直接查询 DB 的 get_permissions_list_sync(),避免跨事件循环问题

前端权限控制

v-perm 指令

前端使用自定义 v-perm 指令控制按钮显隐:

html
<!-- 只有拥有 sys:user:add 权限的用户才能看到此按钮 -->
<el-button v-perm="'sys:user:add'" type="primary">新增</el-button>

<!-- 多个权限,满足其一即可 -->
<el-button v-perm="['sys:user:update', 'sys:user:delete']">操作</el-button>

路由守卫

前端路由守卫根据用户菜单权限动态生成路由,无权限的页面不会出现在导航菜单中。

权限数据查询

权限列表通过角色-菜单关联查询:

python
# 获取用户的权限节点列表(带 Redis 缓存)
async def get_permissions_list(user_id: int) -> list[str]:
 # 1. 先查 Redis 缓存
 # 2. 缓存未命中 → 查 DB(user → role → menu where type=1)
 # 3. 返回 permission 字段列表
 pass

# 同步版本(供同步端点使用)
def get_permissions_list_sync(user_id: int) -> list[str]:
 # 直接查 DB,不走缓存
 pass

总结

的 RBAC 权限方案具备以下特点:

1. 三层模型:用户-角色-菜单,支持灵活的角色分配
2. 节点权限:sys:module:action 格式,细粒度控制每个操作
3. 前后端统一:后端 @permission_required + 前端 v-perm 指令
4. 管理员免检:userId=1 自动跳过权限校验
5. 双路校验:异步端点走 Redis 缓存,同步端点直查 DB(避免跨事件循环)
6. 菜单路由:type=0 菜单生成前端路由,type=1 节点控制按钮权限
7. 懒加载:装饰器内懒加载业务 service,避免 core → 业务模块顶层循环依赖

小蚂蚁云团队 · 提供技术支持