Files
leaudit-platform-backend/docs/老系统_docauditai_用户权限架构深度分析.md
T
2026-04-29 15:23:19 +08:00

1048 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 老系统 `docauditai` 用户权限架构深度分析
本文档用于系统性梳理老项目:
- `/home/wren-dev/Porject/docauditai`
中的用户、认证、角色、路由、权限、地区、数据范围逻辑。
目标不是简单列表,而是明确回答:
- 老系统真实的用户权限架构是什么
- 它依赖哪些表和哪些业务规则
- 新系统应该继承什么、舍弃什么、升级什么
---
## 1. 结论先行
老系统并不是一个“只有 6 张 RBAC 表”的简单权限系统。
它真实的架构是:
- 统一登录入口:OAuth + 账密登录
- 用户主表:`sso_users`
- 角色体系:`roles + user_role`
- 菜单路由:`sys_routes + role_route`
- 权限点:`permissions + role_permissions`
- 数据权限:`role_permissions.data_scope`
- 地区隔离:大量业务依赖 `sso_users.area`
- 组织同步:登录时通过 MQ / 组织表同步 `area / tenant / dep`
也就是说,老系统本质上是:
- `RBAC + area-based data scope`
而不是纯粹的:
- `用户 - 角色 - 菜单`
---
## 2. 核心表与模块
从代码实际使用看,老系统核心不只是以下 6 张表:
- `sys_routes`
- `sso_users`
- `roles`
- `role_route`
- `role_permissions`
- `user_role`
还必须加上:
- `permissions`
- `route_permission`
- 业务表中的 `area` 字段
- `roles.data_scope`
- `role_permissions.data_scope`
相关代码位置:
- 认证:`app/routes/auth.py`
- JWT`app/auth/auth.py`
- 路由权限:`app/rbac/route_permission.py`
- 权限检查:`app/rbac/permission_checker_v2.py`
- 数据范围:`app/rbac/data_scope_injector_v2.py`
- RBAC API`app/routes/v3/rbac.py`
---
## 3. 登录架构
## 3.1 统一登录入口
老系统登录主入口是:
- `POST /auth/login`
代码:
- `app/routes/auth.py`
统一登录入口自动识别两种模式:
- OAuth 登录
- 账号密码登录
识别规则:
- 请求体包含 `userInfo.sub` → OAuth 登录
- 请求体包含 `username + password` → 账密登录
对应代码:
- `app/routes/auth.py:111`
- `app/routes/auth.py:154`
- `app/routes/auth.py:158`
这说明老系统在登录入口设计上已经做到了统一。
---
## 3.2 OAuth 登录逻辑
OAuth 登录处理函数:
- `app/routes/auth.py:_handle_oauth_login`
主要流程:
1. 读取前端传入的 `userInfo`
2. 通过 `sub` 查询 `sso_users`
3. 如果用户不存在,则自动创建用户
4. 如果用户已存在,则更新用户基本资料
5. 查询用户角色
6. 生成 JWT
7. 返回统一登录响应
对应代码位置:
- `app/routes/auth.py:412`
- `app/routes/auth.py:451`
- `app/routes/auth.py:518`
- `app/routes/auth.py:563`
- `app/routes/auth.py:601`
- `app/routes/auth.py:611`
### OAuth 用户创建规则
当用户不存在时:
- 自动插入 `sso_users`
- 自动分配默认角色 `common`
对应代码:
- `app/routes/auth.py:471`
- `app/routes/auth.py:512`
这意味着:
- 老系统支持 OAuth 首登自动建号
- 并且会自动授予最低默认权限
---
## 3.3 账号密码登录逻辑
密码登录处理函数:
- `app/routes/auth.py:_handle_password_login`
主要流程:
1. 根据 `username/sub` 查询 `sso_users`
2. 校验密码
3. 校验状态 / 删除标记
4. 同步组织信息(如果有)
5. 查询角色
6. 生成 JWT
7. 返回统一响应
对应代码:
- `app/routes/auth.py:655`
- `app/routes/auth.py:689`
- `app/routes/auth.py:723`
- `app/routes/auth.py:745`
- `app/routes/auth.py:756`
- `app/routes/auth.py:798`
可以看出,老系统并没有把账密登录和 OAuth 登录做成两套完全不同的人群模型。
它们最终都归并到:
- `sso_users`
- `user_role`
- `roles`
- JWT
这是一个非常重要的架构特征。
---
## 4. JWT 模型
JWT 逻辑在:
- `app/auth/auth.py`
JWT Payload 中实际携带的关键字段有:
- `user_id`
- `username`
- `user_role`
- `permissions`(可选)
- `sub`
- `nick_name`
- `email`
- `phone_number`
- `ou_id`
- `ou_name`
- `is_leader`
- `area`
对应代码:
- `app/auth/auth.py:61`
- `app/auth/auth.py:94`
- `app/auth/auth.py:101`
- `app/auth/auth.py:106`
JWT 解码后,同样会恢复成:
- `User`
- `TokenData`
对应:
- `app/auth/auth.py:24`
- `app/auth/auth.py:46`
- `app/auth/auth.py:119`
- `app/auth/auth.py:171`
### 结论
老系统 JWT 不只是认证 token,它还承担了:
- 用户身份
- 用户角色
- 用户地区
- 组织基础信息
其中 `area` 是最关键的业务字段之一。
---
## 5. 用户地区到底怎么来的
这是老系统最关键的逻辑之一。
### 5.1 不是靠前端端口
老系统的地区不是通过前端访问端口推断。
它主要通过组织主数据反查得到。
### 5.2 OAuth 登录时的地区来源
OAuth 登录会调用:
- `_get_user_org_info_from_mq`
位置:
- `app/routes/auth.py:280`
查询流程:
1.`ou_id + nickname` 查询 `um_personinfo`
2. 获取:
- `tenant_uuid`
- `dep_uuid`
- `org_uuid`
3. 再查:
- `um_tenant`
- `um_department`
4. 最终组装:
- `area`
- `tenant_name`
- `dep_name`
- `dep_short_name`
- `ou_name`
其中 `area` 的来源是:
- `tenant_short_name`
-`tenant_name`
对应代码:
- `app/routes/auth.py:206`
- `app/routes/auth.py:263`
- `app/routes/auth.py:280`
- `app/routes/auth.py:380`
- `app/routes/auth.py:435`
### 5.3 用户首次创建时
新用户创建时,组织信息会直接写入 `sso_users`
- `area`
- `tenant_name`
- `dep_name`
- `dep_short_name`
- `ou_name`
对应:
- `app/routes/auth.py:471`
- `app/routes/auth.py:480`
- `app/routes/auth.py:512`
### 5.4 老用户每次登录时
如果用户已存在,每次 OAuth 登录仍会同步这些字段:
- `area`
- `tenant_name`
- `dep_name`
- `dep_short_name`
- `ou_name`
对应:
- `app/routes/auth.py:518`
- `app/routes/auth.py:543`
- `app/routes/auth.py:552`
- `app/routes/auth.py:593`
### 结论
老系统对地区的真实定义是:
- 地区是组织主数据的一部分
- 地区由后端通过 MQ/组织系统同步
- 登录时会把地区同步进 `sso_users.area`
- 之后大量业务依赖这个 `area`
这说明:
- 老系统是“后端主数据决定地区”
- 不是“前端端口决定地区”
---
## 6. 用户主表 `sso_users` 的真实角色
从代码实际用法来看,`sso_users` 不只是登录表。
它承担了:
- 统一身份映射表
- 用户基础档案表
- 地区信息存储表
- 组织信息缓存表
- 登录失败次数 / 锁定状态记录表
常见使用字段:
- `id`
- `sub`
- `username`
- `nick_name`
- `phone_number`
- `email`
- `ou_id`
- `ou_name`
- `is_leader`
- `status`
- `deleted_at`
- `password`
- `try_count`
- `try_login_time`
- `area`
- `tenant_name`
- `dep_name`
- `dep_short_name`
对应代码位置:
- `app/routes/auth.py:451`
- `app/routes/auth.py:689`
- `app/routes/auth.py:914`
### 结论
老系统把 `sso_users` 作为了:
- 用户唯一主表
- 认证与组织信息的汇聚中心
这一点新系统应该继承。
---
## 7. 角色体系是什么
角色相关表:
- `roles`
- `user_role`
### 7.1 用户和角色关系
一个用户可以拥有多个角色。
角色查询通常通过:
- `user_role -> roles`
对应代码:
- `app/routes/auth.py:840`
- `app/routes/auth.py:852`
- `app/routes/auth.py:862`
### 7.2 OAuth 新用户默认角色
新用户默认自动分配:
- `common`
对应代码:
- `app/routes/auth.py:512`
### 7.3 角色本身带数据范围
老系统 `roles` 表本身带:
- `data_scope`
在 RBAC API 中大量出现:
- `RoleService.list_roles`
- `RoleService.get_role`
对应代码:
- `app/services/rbac/role_service.py:98`
- `app/services/rbac/role_service.py:171`
- `app/services/rbac/user_role_service.py:217`
### 结论
老系统角色不只是页面访问角色,它还是:
- 权限角色
- 数据范围角色
也就是说:
- 一个角色同时决定功能权限和数据权限
---
## 8. 菜单 / 路由权限模型
### 8.1 路由表和角色路由表
老系统页面菜单权限主要基于:
- `sys_routes`
- `role_route`
主逻辑在:
- `app/rbac/route_permission.py`
### 8.2 `get_user_routes(user_id)` 的真实行为
流程:
1. 查用户所有角色
2. 用角色查 `role_route`
3. join `sys_routes`
4. 只返回启用路由
5. 组装成树形结构
6. 还会把该路由相关的权限点附在 `permissions` 字段里
对应:
- `app/rbac/route_permission.py:22`
- `app/rbac/route_permission.py:47`
- `app/rbac/route_permission.py:63`
- `app/rbac/route_permission.py:100`
- `app/rbac/route_permission.py:127`
### 8.3 路由不仅是菜单,还带权限上下文
老系统会在返回路由树时,把页面对应的权限点也挂上去。
这意味着前端不是只拿“菜单”,而是拿:
- 菜单结构
- 页面权限上下文
这是很成熟的一种设计。
---
## 9. 功能权限模型
### 9.1 老系统不是只有 `role_permissions`
真正的功能权限核心是:
- `permissions`
- `role_permissions`
其中:
- `permissions`:权限定义表
- `role_permissions`:角色授权表
### 9.2 权限检查器 V2
代码:
- `app/rbac/permission_checker_v2.py`
它的逻辑是:
1. 查用户所有角色
2. 通过 `role_permissions` 找到权限
3. join `permissions`
4.`permission_key`
5. 支持:
- 精确匹配
- 通配符权限
- `GRANT`
- `DENY`
对应:
- `app/rbac/permission_checker_v2.py:61`
- `app/rbac/permission_checker_v2.py:147`
- `app/rbac/permission_checker_v2.py:199`
### 9.3 权限键格式
权限键采用:
- `module:resource:action`
例如:
- `document:list:read`
- `document:delete:delete`
- `dify:dataset:read`
- `system:rbac:manage`
这套结构非常清晰,推荐新系统继续保留。
### 结论
老系统功能权限的真实设计是:
- `permissions.permission_key` 做标准权限定义
- `role_permissions` 决定角色拥有哪些权限
- 路由权限只是页面层,不能替代 `permissions`
---
## 10. 数据权限模型
这是老系统最关键但最容易被忽略的一层。
### 10.1 数据权限来源
数据权限核心在:
- `role_permissions.data_scope`
- `roles.data_scope`
- 用户 `area`
### 10.2 数据范围注入器
代码:
- `app/rbac/data_scope_injector_v2.py`
定义了三种数据范围:
- `ALL`
- `DEPT`
- `SELF`
对应:
- `app/rbac/data_scope_injector_v2.py:24`
### 10.3 三种范围含义
#### `ALL`
- 查看全部数据
- 不加过滤条件
#### `DEPT`
- 查看本地区数据
- 实现方式是:按 `area` 过滤
#### `SELF`
- 只能查看本人数据
- 实现方式是:按 `user_id` 过滤
对应:
- `app/rbac/data_scope_injector_v2.py:170`
- `app/rbac/data_scope_injector_v2.py:228`
### 10.4 本地区的真实含义
老系统里 `DEPT` 虽然名称叫部门范围,但实际很多地方是:
- 按地区 `area` 过滤
也就是说:
- 它更像“本地市范围”
- 而不是严格的部门树范围
这说明老系统的数据权限其实是:
- `ALL / 地区 / 本人`
而不是严格组织树。
---
## 11. `area` 在老系统中的真实地位
### 11.1 `area` 是核心业务字段
老系统中,`area` 被用于:
- 用户归属地区
- 数据权限过滤条件
- 业务记录写入默认地区
- Dify 知识库选择
- Dify 对话应用过滤
- 评查点地区隔离
### 11.2 Dify 知识库访问
代码:
- `app/routes/v3/dify_area_dataset.py`
逻辑:
- 普通用户:按 `current_user.area` 查本地区知识库
- 省级管理员:可看全部
对应:
- `app/routes/v3/dify_area_dataset.py:39`
- `app/routes/v3/dify_area_dataset.py:57`
- `app/routes/v3/dify_area_dataset.py:67`
### 11.3 Dify 对话应用过滤
代码:
- `app/routes/v3/dify_chat_apps.py`
逻辑:
- 根据 `current_user.area` 返回本地区应用
- 同时可以返回省级应用
对应:
- `app/routes/v3/dify_chat_apps.py:29`
- `app/routes/v3/dify_chat_apps.py:60`
### 11.4 PostgREST 转发层的 area 注入
代码:
- `app/exceptions/global_exc.py`
老系统在某些写操作中,甚至会自动把用户 `area` 写进业务数据。
典型例子:
- `evaluation_points` 表写入时自动填充 `area`
并且:
- `provincial_admin` 会被硬编码成 `省级`
对应代码:
- `app/exceptions/global_exc.py:250`
- `app/exceptions/global_exc.py:292`
- `app/exceptions/global_exc.py:307`
### 结论
在老系统中:
- `area` 不是附属字段
- 而是整套业务隔离体系的核心字段之一
---
## 12. 中间件与鉴权行为
### 12.1 JWT 中间件
代码:
- `app/middleware/jwt_auth.py`
行为:
- 白名单路径跳过
- 其他请求必须有 Bearer Token
- 中间件先做基础校验
- 然后将当前用户信息塞进:
- `request.state.current_user`
塞入字段包括:
- `user_id`
- `username`
- `nick_name`
- `email`
- `phone_number`
- `ou_id`
- `ou_name`
- `is_leader`
- `user_role`
- `area`
对应:
- `app/middleware/jwt_auth.py:49`
- `app/middleware/jwt_auth.py:75`
- `app/middleware/jwt_auth.py:88`
- `app/middleware/jwt_auth.py:89`
### 12.2 路由层再做细粒度权限校验
真正功能权限由:
- `require_permission_v2`
处理,位置:
- `app/rbac/decorators_v2.py`
它会:
-`request.state.current_user``user_id`
-`PermissionCheckerV2.check_permission`
- 无权限则 403
对应:
- `app/rbac/decorators_v2.py:56`
- `app/rbac/decorators_v2.py:92`
### 结论
老系统是两层鉴权:
- 中间件:验 token
- 装饰器/依赖:验功能权限
这个分层设计是合理的。
---
## 13. 老系统角色体系的实际业务语义
根据代码表现,老系统大致存在以下角色层次:
- `provincial_admin`
- `admin`
- `common`
- 以及其他业务角色
### 13.1 `provincial_admin`
特点:
- 高权限角色
- 数据范围通常为 `ALL`
- 某些业务场景会被特殊处理为“省级”
- 可访问全部地区的数据 / 配置
体现位置:
- `app/exceptions/global_exc.py:292`
- `app/routes/v3/dify_area_dataset.py:67`
### 13.2 `admin`
特点:
- 更像市级管理员
- 数据范围通常为本地区
- 很多场景按 `area` 做限制
体现位置:
- `app/services/rbac/user_role_service.py:91`
- `app/exceptions/global_exc.py:229`
### 13.3 `common`
特点:
- 默认普通用户
- 新用户自动分配
- 权限最低
- 数据范围通常为 `SELF` 或较弱的本地区权限
体现位置:
- `app/routes/auth.py:512`
- `app/routes/auth.py:601`
---
## 14. 老系统真实的架构图
如果按实际行为抽象,老系统可以概括为:
### 14.1 用户身份层
- `sso_users`
- `sub`
- OAuth / 本地账密登录
### 14.2 角色层
- `roles`
- `user_role`
### 14.3 页面菜单层
- `sys_routes`
- `role_route`
### 14.4 功能权限层
- `permissions`
- `role_permissions`
- `permission_key`
- `grant_type`
### 14.5 数据权限层
- `role_permissions.data_scope`
- `roles.data_scope`
- 用户 `area`
- 业务表 `area`
### 14.6 组织归属层
- `ou_id`
- `ou_name`
- `tenant_name`
- `dep_name`
- `dep_short_name`
- 通过 MQ / 组织表同步
### 结论
老系统不是简单 RBAC,而是:
- `RBAC + 地区数据隔离 + 组织同步`
---
## 15. 老系统优点
### 15.1 登录入口统一
OAuth 和账密最终走同一条认证主链。
### 15.2 用户主数据集中
所有用户最终都汇总到:
- `sso_users`
### 15.3 路由权限设计成熟
- `sys_routes + role_route`
- 支持树形菜单
- 支持隐藏路由
- 支持角色路由缓存
### 15.4 功能权限设计成熟
- `permissions`
- `role_permissions`
- `grant / deny`
- 通配符匹配
### 15.5 数据范围是正式模型,不是临时过滤
虽然比较粗糙,但已经有:
- `ALL`
- `DEPT`
- `SELF`
### 15.6 地区来自后端主数据
老系统地区来源比“前端端口推断”可靠得多。
---
## 16. 老系统缺点
### 16.1 `area` 过载过重
`area` 同时承担:
- 用户地区
- 数据权限过滤条件
- 业务默认地区
- 规则隔离条件
- 知识库分配条件
语义过重。
### 16.2 数据范围表达力不足
只有三档:
- `ALL`
- `DEPT`
- `SELF`
无法优雅表达:
- 多地区访问
- 指定地区集合
- 复杂跨区权限
### 16.3 大量业务写死 `area` 逻辑
导致:
- 架构耦合重
- 后续改动成本高
- 组织逻辑与业务逻辑混杂
### 16.4 存在角色硬编码
例如:
- `provincial_admin -> 省级`
这类逻辑写死在业务层,不够优雅。
### 16.5 `DEPT` 实际上更像地区范围,不是部门范围
命名容易误导。
---
## 17. 对新系统的启示
## 17.1 必须继承的部分
新系统应继承老系统的这些优点:
- 统一登录入口
- OAuth / 账密统一落主用户表
- `sys_routes + role_route`
- `permissions + role_permissions`
- 标准 `permission_key`
- `grant / deny`
- 数据范围模型概念
- 组织同步决定地区
## 17.2 必须升级的部分
新系统不能原样照搬老系统的缺点。
必须升级为:
- 不再只有一个 `area` 字段承载所有数据权限语义
- 把用户默认地区和可访问地区分开
- 把数据权限从 `DEPT` 升级为正式 `region scope`
- 把组织同步保留,但不要在各业务层到处散写地区逻辑
---
## 18. 新系统设计应如何继承老系统
从老系统出发,新系统最合理的方向不是推翻,而是升级:
### 18.1 保留骨架
- `sso_users`
- `roles`
- `user_role`
- `sys_routes`
- `role_route`
- `permissions`
- `role_permissions`
### 18.2 替换数据权限实现
将老系统:
- `ALL / DEPT / SELF`
升级为:
- `ALL`
- `SELF`
- `HOME_REGION`
- `CUSTOM_REGIONS`
- `PROVINCIAL`
### 18.3 组织同步仍保留
地区来源仍然应该以后端组织主数据为准,而不是前端推断。
### 18.4 将 `area` 升级为正式 `region_code`
业务表中的地区字段建议统一收口成稳定 code,而不是直接到处使用中文字符串。
---
## 19. 一句话总结
老系统 `docauditai` 的真实用户权限架构不是“6 张表的简单角色系统”,而是:
-`sso_users` 为用户主数据中心
- 通过 OAuth / 账密统一登录
- 通过组织系统同步 `area / tenant / dep`
-`roles + user_role + sys_routes + role_route + permissions + role_permissions`
管功能权限
-`data_scope + area`
管数据权限
本质是:
- `RBAC + 地区数据权限 + 组织同步`
这就是新系统设计时真正应该继承的老逻辑骨架。