企业级权限系统怎么设计二:权限点模型
这篇文章也是我在实施项目中的一些思考和复盘。
上回说到,我们将审批相关的权限逻辑从主权限模型中独立出来。那接下来的问题是:角色和功能之间的权限关系,应该怎么设计?
我们目前是通过“功能”来控制权限,但“功能”到底该定义到什么粒度?应该存哪些信息?这类问题,网上有很多权限模型可以参考,都挺清晰,但一旦需求复杂起来,如果没有足够的前瞻性设计,盲目“抄模板”或者硬造轮子,就容易出问题:
- 权限控制变得难以扩展、难以维护;
- 无法覆盖实际业务;
- 甚至上线后还可能出生产事故。
一、功能权限的颗粒度不足
在上一篇文章《企业级权限系统怎么设计 —— 优秀领域模型的价值》中我们提到,部分业务场景下需要更细的权限控制,比如“查询”、“经办”、“复核”等。
我们当时的方案是直接在功能权限表中加标志位:
can_view、can_execute、can_review
这种方式虽然简单,但本质上是直接用业务结构建模,缺乏抽象,导致几个问题:
- 无法控制到按钮或 API 的级别;
- 策略配置变得受限(如不同渠道的差异化权限);
二、抽象:从功能标志位到权限点
我们思考一下,需求的关键点是:“权限”是为了控制,而“菜单”只是为了展示。所以我们需要将这两者分离,让控制逻辑独立于前端结构。
于是我们引入了一个新的抽象层 —— 权限点(Permission Point)。
概念厘清
- 功能:指为用户提供的业务能力,通常对应一个完整的流程或操作入口。
- 菜单:是功能的前端表现形式,可以是导航菜单、页面按钮,或其他 UI 元素。
- 权限点:是权限控制的最小单位,通常对应一个具体的动作,比如“创建转账订单”、“复核转账订单”等。
抽象后的设计原则
- 前端:根据权限点决定菜单和按钮是否展示;
- 后端:根据权限点决定请求是否放行;
- 功能:作为权限点的组织容器,方便管理和启用;
- 菜单:仅作为展示载体,通过权限点绑定控制可见性。
三、权限点模型设计
设计模型如下:
erDiagram
CUSTOMER ||--o{ USER : "拥有用户"
CUSTOMER ||--o{ ACCOUNT : "拥有账户"
CUSTOMER ||--o{ ROLE : "拥有角色"
CUSTOMER ||--o{ CUSTOMER_FUNCTION : "开通功能"
USER ||--o{ USER_ROLE : "绑定角色"
USER ||--o{ USER_ACCOUNT : "管理账户"
ACCOUNT ||--o{ USER_ACCOUNT : ""
ROLE ||--o{ USER_ROLE : ""
ROLE ||--o{ ROLE_PERMISSION : "绑定权限点"
FUNCTION ||--o{ PERMISSION_POINT : "包含权限点"
FUNCTION ||--o{ CUSTOMER_FUNCTION : ""
PERMISSION_POINT ||--o{ ROLE_PERMISSION : ""
PERMISSION_POINT ||--o{ MENU_PERMISSION : ""
MENU ||--o{ MENU_PERMISSION : ""
erDiagram
%% 客户实体
CUSTOMER {
int id PK "客户ID"
string name "客户名称"
}
%% 用户实体
USER {
int id PK "用户ID"
int customer_id FK "所属客户ID"
string username "用户名"
string password "密码"
}
%% 账户实体
ACCOUNT {
int id PK "账户ID"
int customer_id FK "所属客户ID"
string account_number "账户编号"
}
%% 角色实体
ROLE {
int id PK "角色ID"
int customer_id FK "所属客户ID"
string name "角色名称"
}
%% 功能实体(功能点集合容器)
FUNCTION {
int id PK "功能ID"
string name "功能名称"
string function_type "功能类型:基础管理/业务功能"
}
%% 客户功能开通(功能点约束范围)
CUSTOMER_FUNCTION {
int id PK "客户功能ID"
int customer_id FK "所属客户ID"
int function_id FK "功能ID"
}
%% 权限点实体(最小控制单位)
PERMISSION_POINT {
int id PK "权限点ID"
string code "权限点编码"
string name "权限点名称"
int function_id FK "所属功能"
string operation_type "操作类型(查询/经办/复核)"
string channel "适用渠道"
boolean is_sensitive "是否敏感操作"
boolean requires_approval "是否需要审批"
}
%% 角色-权限点关系(授权)
ROLE_PERMISSION {
int role_id FK "角色ID"
int permission_point_id FK "权限点ID"
}
%% 用户-角色关系
USER_ROLE {
int user_id FK "用户ID"
int role_id FK "角色ID"
}
%% 用户-账户关系
USER_ACCOUNT {
int user_id FK "用户ID"
int account_id FK "账户ID"
}
%% 菜单实体(用于前端控制)
MENU {
int id PK "菜单ID"
string name "菜单名称"
int parent_id "父菜单ID"
string route "路由路径"
}
%% 菜单绑定权限点
MENU_PERMISSION {
int menu_id FK "菜单ID"
int permission_point_id FK "权限点ID"
}
%% 实体关系定义
CUSTOMER ||--o{ USER : "拥有用户"
CUSTOMER ||--o{ ACCOUNT : "拥有账户"
CUSTOMER ||--o{ ROLE : "拥有角色"
CUSTOMER ||--o{ CUSTOMER_FUNCTION : "开通功能"
USER ||--o{ USER_ROLE : "绑定角色"
USER ||--o{ USER_ACCOUNT : "管理账户"
ACCOUNT ||--o{ USER_ACCOUNT : ""
ROLE ||--o{ USER_ROLE : ""
ROLE ||--o{ ROLE_PERMISSION : "绑定权限点"
FUNCTION ||--o{ PERMISSION_POINT : "包含权限点"
FUNCTION ||--o{ CUSTOMER_FUNCTION : ""
PERMISSION_POINT ||--o{ ROLE_PERMISSION : ""
PERMISSION_POINT ||--o{ MENU_PERMISSION : ""
MENU ||--o{ MENU_PERMISSION : ""
核心表说明
表名 | 说明 |
---|---|
PERMISSION_POINT | 权限点,最小控制单位,如:TRANSFER_QUERY、TRANSFER_CREATE、TRANSFER_APPROVE |
ROLE_PERMISSION | 角色与权限点的绑定关系 |
CUSTOMER_FUNCTION | 控制某客户是否启用某功能(影响其下属权限点是否生效) |
MENU_PERMISSION | 菜单与权限点的绑定关系,决定前端菜单是否展示 |
四、模型优势与实践建议
✦ 多渠道适配
权限点中加上 channel
字段,可区分不同渠道(如 PC / H5 / App)下的权限差异。
✦ 敏感控制支持
支持通过 is_sensitive
和 requires_approval
字段,控制是否为敏感操作、是否需要审批等策略扩展。
✦ 降低客户配置复杂度
为客户预设默认角色和权限包,不必手动分配每一个权限点,提升开通效率。
✦ 菜单渲染统一化
前端登录后根据权限点统一返回菜单结构,可减少各渠道重复开发。
✦ 权限校验后移至后端
前端仅做展示控制,实际访问逻辑必须依赖后端验证权限点,避免“靠 UI 控制安全”的假象。