# 老系统 `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 + 地区数据权限 + 组织同步` 这就是新系统设计时真正应该继承的老逻辑骨架。