Skip to content

RBAC权限

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

权限模型 ​

用户(User) ──N:N── 角色(Role) ──N:N── 菜单(Menu)
 │
 type=0: 菜单(页面路由)
 type=1: 节点(权限按钮)

数据库表关系:

关联表说明
flask_user_role用户-角色关联表
flask_role_menu角色-菜单关联表
flask_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(BaseModel, BaseDB):
    """菜单模型类"""
    # ============================================================
    # 表名配置
    # ============================================================
    __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节点权限按钮,控制操作权限

后端权限校验 ​

check_permission 装饰器 ​

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

python
from src.core.permission import check_permission

@position_blue.route('/add', methods=['POST'])
@check_permission('sys:position:add')
def add():
    from src.modules.system.position.service import add_position
    from src.modules.system.position.schema import PositionForm
    data = parse_request_body(request, PositionForm)
    return add_position(data)

校验逻辑(完整实现):

python
# src/core/permission.py
import fnmatch
import hmac
import logging
from functools import wraps
from typing import List, Protocol

from flask import request, g, current_app
from src.core import response as R
from src.core.security import get_user_id
from config import PERMISSION_ENABLED, INTERNAL_API_KEY, INTERNAL_ALLOWED_IPS

logger = logging.getLogger(__name__)


class PermissionProvider(Protocol):
    """权限数据提供方接口:core 只认接口,不认实现。"""

    def get_user_permissions(self, user_id: int) -> List[str]:
        """返回用户拥有的权限节点列表。"""


def get_provider() -> "PermissionProvider":
    """获取注入的权限提供方(应用工厂注入)。"""
    return current_app.extensions.get("permission_provider")


def check_permission(permission: str, log: bool = False):
    """
    权限验证装饰器

    权限验证规则:
        1. 定时任务等内部调用 → 跳过权限验证,直接放行
        2. 超级管理员(user_id=1) → 拥有所有权限
        3. 普通用户 → 通过注入的 PermissionProvider 验证权限
    """
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # 定时任务等内部调用:跳过权限验证
            if is_internal_call():
                return func(*args, **kwargs)

            # 获取用户ID
            user_id = getattr(g, "user_id", None)
            if user_id is None:
                try:
                    user_id = get_user_id()
                except Exception:
                    user_id = None

            # 超级管理员放行(user_id=1 为系统内置管理员)
            if user_id == 1:
                return func(*args, **kwargs)

            # 普通用户权限验证
            if user_id:
                provider = get_provider()
                if provider is None:
                    return R.failed(msg="权限验证失败", code=500)

                permission_list = provider.get_user_permissions(user_id)
                if permission not in permission_list:
                    return R.failed(msg="权限不足!", code=403)
            else:
                return R.failed(msg="请先登录", code=401)

            return func(*args, **kwargs)
        return wrapper
    return decorator

依赖注入设计

权限校验采用依赖注入设计(解决 core → modules 依赖倒置):

  • core 定义 PermissionProvider 协议,不 import 任何业务模块
  • 应用工厂启动时注入 menu 服务的实现(见 src/app.py 的 _inject_dependencies)
  • check_permission 通过 current_app.extensions['permission_provider'] 获取权限数据

管理员免检

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

权限数据注入 ​

权限数据通过应用工厂注入,避免 core 层直接依赖 modules 层:

python
# src/app.py - _inject_dependencies()
from src.modules.system.menu.service import get_user_permissions

class MenuPermissionProvider:
    """权限数据提供方(适配 menu 服务)。"""
    def get_user_permissions(self, user_id):
        return get_user_permissions(user_id)

app.extensions['permission_provider'] = MenuPermissionProvider()

权限缓存 ​

权限列表通过 Redis 缓存,提高查询性能。缓存在角色菜单变更时主动失效。

# 3. 写入缓存
redis.set(cache_key, json.dumps(permission_list), ex=3600)

return permission_list

## 前端权限控制

### 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 缓存)
def get_permissions_list(user_id: int) -> list:
    # 1. 先查 Redis 缓存
    # 2. 缓存未命中 → 查 DB(user → role → menu where type=1)
    # 3. 返回 permission 字段列表
    pass

权限缓存失效 ​

当用户权限变更时,需要清除对应的缓存:

python
def clear_permission_cache(user_id: int):
    """清除用户权限缓存"""
    cache_key = f"user:permissions:{user_id}"
    redis.delete(cache_key)

权限变更场景:

  1. 用户角色变更
  2. 角色菜单变更
  3. 菜单权限节点变更

总结 ​

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

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

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