Become a sponsor

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 岗位列表# 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 | 节点 | 权限按钮,控制操作权限 |
src/core/access_decorators.py 中的 @permission_required 装饰器用于端点级权限校验:
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)校验逻辑(完整实现):
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 decoratorRequest 提取
装饰器通过 _extract_request() 从函数参数中提取 Request 对象,支持 request 关键字参数或位置参数两种方式。如果端点函数没有 request 参数,权限校验将被跳过。
管理员免检
用户ID为1的管理员账号(admin)自动跳过所有权限校验,拥有全部权限。
装饰器自动适配同步和异步端点:
get_permissions_list(),性能更优get_permissions_list_sync(),避免跨事件循环问题前端使用自定义 v-perm 指令控制按钮显隐:
<!-- 只有拥有 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 缓存)
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 → 业务模块顶层循环依赖