Become a sponsor

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 岗位列表# 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 | 节点 | 权限按钮,控制操作权限 |
src/core/permission.py 中的 @check_permission 装饰器用于端点级权限校验:
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)校验逻辑(完整实现):
# 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 依赖倒置):
PermissionProvider 协议,不 import 任何业务模块src/app.py 的 _inject_dependencies)check_permission 通过 current_app.extensions['permission_provider'] 获取权限数据管理员免检
用户ID为1的管理员账号(admin)自动跳过所有权限校验,拥有全部权限。
权限数据通过应用工厂注入,避免 core 层直接依赖 modules 层:
# 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>前端路由守卫根据用户菜单权限动态生成路由,无权限的页面不会出现在导航菜单中。
权限列表通过角色-菜单关联查询:
# 获取用户的权限节点列表(带 Redis 缓存)
def get_permissions_list(user_id: int) -> list:
# 1. 先查 Redis 缓存
# 2. 缓存未命中 → 查 DB(user → role → menu where type=1)
# 3. 返回 permission 字段列表
pass当用户权限变更时,需要清除对应的缓存:
def clear_permission_cache(user_id: int):
"""清除用户权限缓存"""
cache_key = f"user:permissions:{user_id}"
redis.delete(cache_key)权限变更场景:
项目的 RBAC 权限方案具备以下特点:
1. 三层模型:用户-角色-菜单,支持灵活的角色分配
2. 节点权限:sys:module:action 格式,细粒度控制每个操作
3. 前后端统一:后端 @check_permission + 前端 v-perm 指令
4. 管理员免检:userId=1 自动跳过权限校验
5. 权限缓存:Redis 缓存权限列表,提高查询性能
6. 菜单路由:type=0 菜单生成前端路由,type=1 节点控制按钮权限
7. 懒加载:装饰器内懒加载业务 service,避免 core → 业务模块顶层循环依赖