8.8 KiB
权限系统全面修复设计文档
背景
SQL 模型已定义完整的权限体系(sys_menu, sys_role, sys_role_menu, sys_role_data_scope_detail, sys_user_role),但后端和前端只实现了 CRUD 和 UI 配置,权限执行层几乎为空。本次修复补全所有缺失的权限执行逻辑。
修复范围
| # | 模块 | 优先级 | 描述 |
|---|---|---|---|
| 1 | 后端菜单权限守卫 | P0 | 将已有的 require_menu_code() 应用到所有业务路由 |
| 2 | 后端数据权限过滤 | P0 | 创建 data_scope 服务,在业务查询中按部门/用户/自定义范围过滤 |
| 3 | 前端用户角色选择器 | P1 | 用户管理弹窗中添加角色多选下拉框 |
| 4 | 前端按钮级权限 | P1 | 业务页面按钮添加 v-auth / hasAuth 权限控制 |
| 5 | admin 无权限码安全漏洞 | P2 | 修复空权限码返回 {all: true} 的问题 |
| 6 | 多角色权限合并 | P2 | 后端菜单码已天然合并;数据权限需实现并集逻辑 |
模块 1:后端菜单权限守卫
现状
app/core/menu_guard.py已定义require_menu_code(menu_code)函数- 函数逻辑:super_admin 放行 → 查用户菜单权限码 → 不匹配则 403
- 但从未在任何路由中 import 或使用
修改方案
在业务路由的每个端点上叠加 Depends(require_menu_code("xxx")) 作为第二层守卫。
原则:
- 系统管理接口(租户、角色、菜单 CRUD)保持
require_admin_or_super_admin,仅 super_admin 可操作 - 业务接口使用
require_menu_code - 列表接口用
xxx:list,创建用xxx:add,更新用xxx:edit,删除用xxx:delete require_menu_code内部已先调用require_admin_or_super_admin,所以路由只需一个 Depends
影响文件(注:业务 API 文件已在工程清理中移除,仅 admin.py / workflow.py / leave.py 保留):
backend/app/api/admin.py— 系统管理部分(用户管理加system:user:list等)backend/app/api/workflow.py— 工作流接口backend/app/api/leave.py— 请假申请
perms 值来源:直接使用 sys_menu 表中已定义的 perms 字段值。
需要新增的 sys_menu 按钮(F)类型记录
当前 SQL 中 sys_menu 大多只有 M(目录) 和 C(页面) 类型。为了让 require_menu_code 能精确控制到增删改操作,需要在 SQL 中为每个 C 类型菜单补充 F 类型按钮权限:
xxx:list— 查看列表xxx:add— 新增xxx:edit— 编辑xxx:delete— 删除
这些 F 类型记录的 parent_id 指向对应的 C 类型菜单。此步骤为 数据初始化,需生成 SQL 脚本。
模块 2:后端数据权限过滤
现状
sys_role.data_scope字段存在(1-5 级别)sys_role_data_scope_detail表和 CRUD 已实现- 没有任何业务查询读取 data_scope 进行过滤
设计
新文件:backend/app/core/data_scope.py
def apply_data_scope(query, model, user_info: dict, db: Session) -> query:
"""在 SQLAlchemy query 上叠加数据权限过滤条件。
参数:
query: 原始 SQLAlchemy query 对象
model: ORM 模型类(需要有 create_dept_id, create_by 字段)
user_info: get_current_user 返回的 dict(含 user_id, dept_id)
db: 数据库 session
返回:
添加了数据权限过滤条件的 query
"""
过滤逻辑
| data_scope | 条件 |
|---|---|
1 全部数据 |
无附加过滤 |
2 本部门 |
model.create_dept_id == user.dept_id |
3 本部门及以下 |
model.create_dept_id IN (user.dept_id + 子部门ID列表) |
4 仅本人 |
model.create_by == user.user_id |
5 自定义 |
并集:(create_dept_id IN 自定义部门) OR (create_by IN 自定义用户) |
多角色合并(并集语义)
用户可能有多个角色,每个角色有自己的 data_scope。合并策略:
- 任一角色 data_scope=1(全部) → 不过滤
- 否则,收集所有角色的有效过滤范围,取并集
- 并集计算:将所有 scope 2/3 转为具体部门 ID 集合,scope 4 转为用户 ID 集合,scope 5 直接用自定义范围 → 全部合并后构建
OR条件
集成方式
在 admin.py 等路由的 list 接口中:
@router.get("/customers")
async def list_customers(
...,
user: dict = Depends(require_menu_code("biz:customer:list")),
db: Session = Depends(get_db),
):
query = db.query(BizCustomer).filter(BizCustomer.del_flag == 0, ...)
query = apply_data_scope(query, BizCustomer, user, db)
...
依赖信息
需要 user_info dict 中包含 dept_id。当前 JWT payload 中 region_id 即为 dept_id(在 public.py 登录时已设置)。
模块 3:前端用户角色选择器
现状
- 后端
UserCreateBody/UserUpdateBody已支持roleIds字段 - 后端
GET /web/admin/roles/options已返回角色下拉列表 web-admin/src/views/system/user/index.vue表单中没有角色选择器
修改方案
在 user/index.vue 的创建/编辑弹窗中:
- 添加角色多选下拉框(
el-select multiple) - 调用
GET /web/admin/roles/options获取角色列表 - formData 中添加
roleIds字段 - 提交时一并传给后端
参考:modules/user-dialog.vue 中已有类似实现,可参考其角色选择逻辑。
模块 4:前端按钮级权限
现状
v-auth指令已实现(读取route.meta.authList)hasAuth()composable 已实现- 但实际业务页面中几乎未使用
修改方案
在每个业务页面的操作按钮上添加权限控制:
<!-- 新增按钮 -->
<el-button v-if="hasAuth('add')" type="primary" @click="handleAdd">新增</el-button>
<!-- 编辑按钮 -->
<el-button v-if="hasAuth('edit')" link type="primary" @click="handleEdit(row)">编辑</el-button>
<!-- 删除按钮 -->
<el-button v-if="hasAuth('delete')" link type="danger" @click="handleDelete(row)">删除</el-button>
权限标识来源:sys_menu 中 menu_type='F' 的按钮权限记录的 perms 字段。
注意:当前 v-auth 指令从 route.meta.authList 读取,而 hasAuth() 从 userStore.getUserInfo.buttons 读取。需确保按钮权限列表在登录时正确加载到这两个位置。
涉及页面
src/views/system/user/index.vuesrc/views/system/role/index.vue(角色管理本身也需按钮权限)src/views/basic-info/customer/index.vuesrc/views/basic-info/cell-sample-type/index.vuesrc/views/basic-info/collection-region/index.vuesrc/views/basic-info/collection-protocol-no/index.vuesrc/views/basic-info/sample-code/index.vue- 其他业务页面
模块 5:admin 无权限码安全修复
现状
backend/app/api/public.py 中 /web/auth/menu-codes 端点:
if not codes:
return {"isSuperAdmin": False, "all": True, "menuCodes": []} # ← 安全漏洞
当 admin 用户没有任何菜单权限时,返回 all: true,前端将此视为拥有所有权限。
修改方案
后端:当 codes 为空时返回 all: false,不再赋予全部权限。
if not codes:
return {"isSuperAdmin": False, "all": False, "menuCodes": []}
前端:确保 MenuProcessor.ts 正确处理 all: false + 空 menuCodes 的情况——显示无菜单/无权限页面。
模块 6:多角色权限合并
后端菜单权限码
iter_user_menu_perm_codes() 已通过 sys_user_role → sys_role_menu → sys_menu 链路查询,天然支持多角色菜单并集(DISTINCT 去重)。无需修改。
后端数据权限
新增函数 resolve_user_data_scope_union(db, user_id) -> DataScopeFilter:
- 查询用户所有有效角色的
data_scope和scope_details - 合并策略:
- 任一角色 scope=1 → 返回 "全部"(不过滤)
- 收集所有角色的有效范围:
- scope=2 → 加入 user.dept_id 到部门集合
- scope=3 → 加入 user.dept_id + 所有子部门到部门集合
- scope=4 → 加入 user.user_id 到用户集合
- scope=5 → 加入 scope_details 中的 dept 和 user 到各自集合
- 最终条件:
create_dept_id IN 部门集合 OR create_by IN 用户集合
- 返回统一的过滤条件对象
不在本次修复范围
- 菜单祖先自动包含(P3,前端已通过 el-tree 不设 check-strictly 间接处理)
- 工作流权限控制(独立模块,另行设计)
- 小程序端权限(独立模块)
影响评估
- 后端:新增 1 个文件(data_scope.py),修改 ~6 个路由文件
- 前端:修改 ~10 个页面文件,不新增文件
- 数据库:新增 sys_menu F 类型按钮权限记录的 SQL 脚本
- 风险:低。所有修改基于已有的基础设施,不改变数据模型