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

18 KiB
Raw Blame History

老系统 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
  • JWTapp/auth/auth.py
  • 路由权限:app/rbac/route_permission.py
  • 权限检查:app/rbac/permission_checker_v2.py
  • 数据范围:app/rbac/data_scope_injector_v2.py
  • RBAC APIapp/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_useruser_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 + 地区数据权限 + 组织同步

这就是新系统设计时真正应该继承的老逻辑骨架。