18 KiB
老系统 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_routessso_usersrolesrole_routerole_permissionsuser_role
还必须加上:
permissionsroute_permission- 业务表中的
area字段 roles.data_scoperole_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:111app/routes/auth.py:154app/routes/auth.py:158
这说明老系统在登录入口设计上已经做到了统一。
3.2 OAuth 登录逻辑
OAuth 登录处理函数:
app/routes/auth.py:_handle_oauth_login
主要流程:
- 读取前端传入的
userInfo - 通过
sub查询sso_users - 如果用户不存在,则自动创建用户
- 如果用户已存在,则更新用户基本资料
- 查询用户角色
- 生成 JWT
- 返回统一登录响应
对应代码位置:
app/routes/auth.py:412app/routes/auth.py:451app/routes/auth.py:518app/routes/auth.py:563app/routes/auth.py:601app/routes/auth.py:611
OAuth 用户创建规则
当用户不存在时:
- 自动插入
sso_users - 自动分配默认角色
common
对应代码:
app/routes/auth.py:471app/routes/auth.py:512
这意味着:
- 老系统支持 OAuth 首登自动建号
- 并且会自动授予最低默认权限
3.3 账号密码登录逻辑
密码登录处理函数:
app/routes/auth.py:_handle_password_login
主要流程:
- 根据
username/sub查询sso_users - 校验密码
- 校验状态 / 删除标记
- 同步组织信息(如果有)
- 查询角色
- 生成 JWT
- 返回统一响应
对应代码:
app/routes/auth.py:655app/routes/auth.py:689app/routes/auth.py:723app/routes/auth.py:745app/routes/auth.py:756app/routes/auth.py:798
可以看出,老系统并没有把账密登录和 OAuth 登录做成两套完全不同的人群模型。
它们最终都归并到:
sso_usersuser_roleroles- JWT
这是一个非常重要的架构特征。
4. JWT 模型
JWT 逻辑在:
app/auth/auth.py
JWT Payload 中实际携带的关键字段有:
user_idusernameuser_rolepermissions(可选)subnick_nameemailphone_numberou_idou_nameis_leaderarea
对应代码:
app/auth/auth.py:61app/auth/auth.py:94app/auth/auth.py:101app/auth/auth.py:106
JWT 解码后,同样会恢复成:
UserTokenData
对应:
app/auth/auth.py:24app/auth/auth.py:46app/auth/auth.py:119app/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
查询流程:
- 用
ou_id + nickname查询um_personinfo - 获取:
tenant_uuiddep_uuidorg_uuid
- 再查:
um_tenantum_department
- 最终组装:
areatenant_namedep_namedep_short_nameou_name
其中 area 的来源是:
tenant_short_name- 或
tenant_name
对应代码:
app/routes/auth.py:206app/routes/auth.py:263app/routes/auth.py:280app/routes/auth.py:380app/routes/auth.py:435
5.3 用户首次创建时
新用户创建时,组织信息会直接写入 sso_users:
areatenant_namedep_namedep_short_nameou_name
对应:
app/routes/auth.py:471app/routes/auth.py:480app/routes/auth.py:512
5.4 老用户每次登录时
如果用户已存在,每次 OAuth 登录仍会同步这些字段:
areatenant_namedep_namedep_short_nameou_name
对应:
app/routes/auth.py:518app/routes/auth.py:543app/routes/auth.py:552app/routes/auth.py:593
结论
老系统对地区的真实定义是:
- 地区是组织主数据的一部分
- 地区由后端通过 MQ/组织系统同步
- 登录时会把地区同步进
sso_users.area - 之后大量业务依赖这个
area
这说明:
- 老系统是“后端主数据决定地区”
- 不是“前端端口决定地区”
6. 用户主表 sso_users 的真实角色
从代码实际用法来看,sso_users 不只是登录表。
它承担了:
- 统一身份映射表
- 用户基础档案表
- 地区信息存储表
- 组织信息缓存表
- 登录失败次数 / 锁定状态记录表
常见使用字段:
idsubusernamenick_namephone_numberemailou_idou_nameis_leaderstatusdeleted_atpasswordtry_counttry_login_timeareatenant_namedep_namedep_short_name
对应代码位置:
app/routes/auth.py:451app/routes/auth.py:689app/routes/auth.py:914
结论
老系统把 sso_users 作为了:
- 用户唯一主表
- 认证与组织信息的汇聚中心
这一点新系统应该继承。
7. 角色体系是什么
角色相关表:
rolesuser_role
7.1 用户和角色关系
一个用户可以拥有多个角色。
角色查询通常通过:
user_role -> roles
对应代码:
app/routes/auth.py:840app/routes/auth.py:852app/routes/auth.py:862
7.2 OAuth 新用户默认角色
新用户默认自动分配:
common
对应代码:
app/routes/auth.py:512
7.3 角色本身带数据范围
老系统 roles 表本身带:
data_scope
在 RBAC API 中大量出现:
RoleService.list_rolesRoleService.get_role
对应代码:
app/services/rbac/role_service.py:98app/services/rbac/role_service.py:171app/services/rbac/user_role_service.py:217
结论
老系统角色不只是页面访问角色,它还是:
- 权限角色
- 数据范围角色
也就是说:
- 一个角色同时决定功能权限和数据权限
8. 菜单 / 路由权限模型
8.1 路由表和角色路由表
老系统页面菜单权限主要基于:
sys_routesrole_route
主逻辑在:
app/rbac/route_permission.py
8.2 get_user_routes(user_id) 的真实行为
流程:
- 查用户所有角色
- 用角色查
role_route - join
sys_routes - 只返回启用路由
- 组装成树形结构
- 还会把该路由相关的权限点附在
permissions字段里
对应:
app/rbac/route_permission.py:22app/rbac/route_permission.py:47app/rbac/route_permission.py:63app/rbac/route_permission.py:100app/rbac/route_permission.py:127
8.3 路由不仅是菜单,还带权限上下文
老系统会在返回路由树时,把页面对应的权限点也挂上去。
这意味着前端不是只拿“菜单”,而是拿:
- 菜单结构
- 页面权限上下文
这是很成熟的一种设计。
9. 功能权限模型
9.1 老系统不是只有 role_permissions
真正的功能权限核心是:
permissionsrole_permissions
其中:
permissions:权限定义表role_permissions:角色授权表
9.2 权限检查器 V2
代码:
app/rbac/permission_checker_v2.py
它的逻辑是:
- 查用户所有角色
- 通过
role_permissions找到权限 - join
permissions - 读
permission_key - 支持:
- 精确匹配
- 通配符权限
GRANTDENY
对应:
app/rbac/permission_checker_v2.py:61app/rbac/permission_checker_v2.py:147app/rbac/permission_checker_v2.py:199
9.3 权限键格式
权限键采用:
module:resource:action
例如:
document:list:readdocument:delete:deletedify:dataset:readsystem:rbac:manage
这套结构非常清晰,推荐新系统继续保留。
结论
老系统功能权限的真实设计是:
permissions.permission_key做标准权限定义role_permissions决定角色拥有哪些权限- 路由权限只是页面层,不能替代
permissions
10. 数据权限模型
这是老系统最关键但最容易被忽略的一层。
10.1 数据权限来源
数据权限核心在:
role_permissions.data_scoperoles.data_scope- 用户
area
10.2 数据范围注入器
代码:
app/rbac/data_scope_injector_v2.py
定义了三种数据范围:
ALLDEPTSELF
对应:
app/rbac/data_scope_injector_v2.py:24
10.3 三种范围含义
ALL
- 查看全部数据
- 不加过滤条件
DEPT
- 查看本地区数据
- 实现方式是:按
area过滤
SELF
- 只能查看本人数据
- 实现方式是:按
user_id过滤
对应:
app/rbac/data_scope_injector_v2.py:170app/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:39app/routes/v3/dify_area_dataset.py:57app/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:29app/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:250app/exceptions/global_exc.py:292app/exceptions/global_exc.py:307
结论
在老系统中:
area不是附属字段- 而是整套业务隔离体系的核心字段之一
12. 中间件与鉴权行为
12.1 JWT 中间件
代码:
app/middleware/jwt_auth.py
行为:
- 白名单路径跳过
- 其他请求必须有 Bearer Token
- 中间件先做基础校验
- 然后将当前用户信息塞进:
request.state.current_user
塞入字段包括:
user_idusernamenick_nameemailphone_numberou_idou_nameis_leaderuser_rolearea
对应:
app/middleware/jwt_auth.py:49app/middleware/jwt_auth.py:75app/middleware/jwt_auth.py:88app/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:56app/rbac/decorators_v2.py:92
结论
老系统是两层鉴权:
- 中间件:验 token
- 装饰器/依赖:验功能权限
这个分层设计是合理的。
13. 老系统角色体系的实际业务语义
根据代码表现,老系统大致存在以下角色层次:
provincial_adminadmincommon- 以及其他业务角色
13.1 provincial_admin
特点:
- 高权限角色
- 数据范围通常为
ALL - 某些业务场景会被特殊处理为“省级”
- 可访问全部地区的数据 / 配置
体现位置:
app/exceptions/global_exc.py:292app/routes/v3/dify_area_dataset.py:67
13.2 admin
特点:
- 更像市级管理员
- 数据范围通常为本地区
- 很多场景按
area做限制
体现位置:
app/services/rbac/user_role_service.py:91app/exceptions/global_exc.py:229
13.3 common
特点:
- 默认普通用户
- 新用户自动分配
- 权限最低
- 数据范围通常为
SELF或较弱的本地区权限
体现位置:
app/routes/auth.py:512app/routes/auth.py:601
14. 老系统真实的架构图
如果按实际行为抽象,老系统可以概括为:
14.1 用户身份层
sso_userssub- OAuth / 本地账密登录
14.2 角色层
rolesuser_role
14.3 页面菜单层
sys_routesrole_route
14.4 功能权限层
permissionsrole_permissionspermission_keygrant_type
14.5 数据权限层
role_permissions.data_scoperoles.data_scope- 用户
area - 业务表
area
14.6 组织归属层
ou_idou_nametenant_namedep_namedep_short_name- 通过 MQ / 组织表同步
结论
老系统不是简单 RBAC,而是:
RBAC + 地区数据隔离 + 组织同步
15. 老系统优点
15.1 登录入口统一
OAuth 和账密最终走同一条认证主链。
15.2 用户主数据集中
所有用户最终都汇总到:
sso_users
15.3 路由权限设计成熟
sys_routes + role_route- 支持树形菜单
- 支持隐藏路由
- 支持角色路由缓存
15.4 功能权限设计成熟
permissionsrole_permissionsgrant / deny- 通配符匹配
15.5 数据范围是正式模型,不是临时过滤
虽然比较粗糙,但已经有:
ALLDEPTSELF
15.6 地区来自后端主数据
老系统地区来源比“前端端口推断”可靠得多。
16. 老系统缺点
16.1 area 过载过重
area 同时承担:
- 用户地区
- 数据权限过滤条件
- 业务默认地区
- 规则隔离条件
- 知识库分配条件
语义过重。
16.2 数据范围表达力不足
只有三档:
ALLDEPTSELF
无法优雅表达:
- 多地区访问
- 指定地区集合
- 复杂跨区权限
16.3 大量业务写死 area 逻辑
导致:
- 架构耦合重
- 后续改动成本高
- 组织逻辑与业务逻辑混杂
16.4 存在角色硬编码
例如:
provincial_admin -> 省级
这类逻辑写死在业务层,不够优雅。
16.5 DEPT 实际上更像地区范围,不是部门范围
命名容易误导。
17. 对新系统的启示
17.1 必须继承的部分
新系统应继承老系统的这些优点:
- 统一登录入口
- OAuth / 账密统一落主用户表
sys_routes + role_routepermissions + role_permissions- 标准
permission_key grant / deny- 数据范围模型概念
- 组织同步决定地区
17.2 必须升级的部分
新系统不能原样照搬老系统的缺点。
必须升级为:
- 不再只有一个
area字段承载所有数据权限语义 - 把用户默认地区和可访问地区分开
- 把数据权限从
DEPT升级为正式region scope - 把组织同步保留,但不要在各业务层到处散写地区逻辑
18. 新系统设计应如何继承老系统
从老系统出发,新系统最合理的方向不是推翻,而是升级:
18.1 保留骨架
sso_usersrolesuser_rolesys_routesrole_routepermissionsrole_permissions
18.2 替换数据权限实现
将老系统:
ALL / DEPT / SELF
升级为:
ALLSELFHOME_REGIONCUSTOM_REGIONSPROVINCIAL
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 + 地区数据权限 + 组织同步
这就是新系统设计时真正应该继承的老逻辑骨架。