给居间团队做系统交接时,我见过一个很典型的状态:权限表上线了,角色也分了五六个,界面看着整齐,但问一句"上个月谁导出过这批客户资料",没人答得上来。
权限模型失败,通常不是权限粒度不够细,而是模型本身漏了三个结构性问题。本文按现象、根因、做法、验证四段展开,代码示例为通用实现思路,不绑定具体框架。
一、现象:权限有了,追溯没有
三个可观察的症状,几乎总是同时出现:
- 员工能看到不属于自己的客户资料,但系统没有任何一条拦截记录;
- 导出功能对所有能登录的账号开放,导出行为不进日志;
- 客户资料与人不对应:一个人离职后,他名下的资料变成"无主数据",只能人工指派。
这三个症状指向同一个问题:权限控制挂在"人"上,而不是挂在"数据主体 + 动作"上。
二、三个失败点为什么会同时出现?
先说两个定义,后面几个判断都依赖它们。
客户资料分级是指按敏感度与业务必要性把客户资料划分成有限档位,并为每一档绑定访问范围、保留期限与处理动作的规则集合。它与"文件分类"不同:分类回答"这是什么文件",分级回答"谁能碰、能碰多久"。
数据域是指权限裁决的最小数据范围单位,通常对应一类资料或一个业务对象。它决定了授权表是按"页面"授权还是按"字段"授权。
三个失败点看起来独立,实际都源于同一个简化:把"能进系统"当成了"能访问数据"。下面逐个拆。
失败点 1:客户主体为什么必须归一?
常见建模是"跟进记录表"作为主表,客户信息作为字段冗余在各条记录里。一旦有两人跟进同一个客户,就会产生两份互不相干的客户信息,权限判断时以哪份为准都说不清。
根因是把过程数据当成了主体数据。正确的关系是:客户主体表唯一,跟进记录、附件、通话记录都是它的从属对象,权限裁决只发生在主体层。
失败点 2:为什么不能拿职位当角色?
把职位直接映射成角色(销售、主管、复核、管理员),再给角色一个"可见范围"字段。这种单轴模型无法表达"主管能看本团队的沟通记录,但不能看证件影像"这类真实需求,于是只能放宽到全可见。
根因是一个角色只能承载一档授权。解法是把授权拆成两个轴:数据域(资料的档位)与动作(查看/编辑/导出/删除),角色只是这两个轴上的一个组合预设。
失败点 3:审计字段挂在业务表上
把last_modified_by、last_modified_at写进客户表,看起来有了留痕。但这类字段只能表达"最后一次",无法表达"谁在什么时候导出过"。
根因是把状态当成了事件。审计需要的是独立的事件流表,只追加不修改,与业务表解耦。
三、做法:三层模型
3.1 主体与档位
客户主体表增加data_level字段,取值为有限枚举。档位不按文件类型定,按泄露后果定,通常 3–4 档足够。
| 字段 | 类型 | 说明 | 是否可空 |
|---|---|---|---|
customer_id | bigint | 主体唯一键 | 否 |
owner_id | bigint | 主责人(唯一) | 否 |
team_id | bigint | 所属团队,用于范围裁决 | 否 |
data_level | tinyint | 资料档位 1–4 | 否 |
retention_until | date | 到期日,由档位+目的终止日推算 | 是 |
frozen | tinyint | 争议/检查冻结标记 | 否 |
retention_until必须落库而不是实时计算:到期清理是一个作业,需要能按字段建索引,而不是每次全表扫描。
3.2 授权矩阵
授权表用(角色 × 数据域 × 动作)三元组,行数可控且可审计。
| 动作 | 档位 1(身份类) | 档位 2(资质类) | 档位 3(过程类) | 档位 4(结果类) |
|---|---|---|---|---|
| 查看 | 主责人、复核岗 | 主责人、主管 | 本团队 | 主管、复核岗 |
| 编辑 | 主责人 | 主责人 | 主责人、主管 | 复核岗 |
| 导出 | 禁止 | 需审批 | 需审批 | 允许并留痕 |
| 删除 | 禁止 | 禁止 | 需审批 | 需审批 |
矩阵的价值在于它是白名单:没有显式授予的动作一律拒绝。默认拒绝比默认允许省掉大量事后排查。
3.3 审计事件流
审计表只追加,记录五个必填维度:
``sql CREATE TABLE audit_event ( id bigint PRIMARY KEY, ts datetime NOT NULL, actor_id bigint NOT NULL, customer_id bigint NULL, -- 批量操作时为空,用 batch_id 关联 batch_id bigint NULL, action tinyint NOT NULL, -- 1查看 2编辑 3导出 4删除 5拦截 field_scope varchar(128) NULL, -- 命中的字段/档位 device_id varchar(64) NULL, result tinyint NOT NULL -- 1放行 0拦截 );``
两个工程细节值得提前定:
- 拦截也要记。
result=0的记录比放行记录更有价值,它证明控制真的在工作。 - 审计写入异步化。同步写审计表会把主流程拖慢,正确做法是先投递到队列,再由消费端落库;但对导出这类高风险动作,可要求"审计未落库则操作失败"。
四、验证:4 个可测指标
权限模型不能用"配置完成"来验收,要用结果验收。
- 越权拦截率:构造 20 组跨档位访问请求,拦截数与总请求数之比应为全部拦截,且每组都产生
result=0的审计记录。 - 导出可还原度:随机抽 10 条导出记录,能否还原出操作人、时间、客户列表、来源终端四项。缺一项即不达标。
- 无主数据率:离职后归属为空的客户数占比。这个指标直接反映主体模型是否归一。
- 到期清单可执行率:生成的到期清单中,能自动进入复核流程的比例;长期停在"已生成未处理"说明流程没有闭环。
四项指标都能量化,也都能在灰度阶段按周观察,适合作为上线门禁。
五、落地顺序
工程上先把客户资料归属拆成两层:存储归属(数据在哪个库、哪个机房)与调阅权归属(谁能读、能读多久)。客户资料归属一旦模糊,权限矩阵就没有裁决对象,档位也只能拍脑袋。所以客户资料归属要在建模阶段定死,而不是等上线后再补。
不建议一次性全量切换。三段灰度更稳:
- 先做主体归一:把客户主体表抽出来,历史记录按人去重挂靠。这一步不动权限,风险较小;
- 再上默认拒绝:授权矩阵先只开"查看",其他动作一律拒绝并记录,观察一周拦截情况,逐条补授权;
- 最后收紧导出:导出改为审批制并接入审计事件流。
顺序反了(先紧导出后做归一)会导致大量正常业务被拦,反弹压力大,最后往往退回默认放开。
七、常见问题
问:为什么不直接用职位做角色?
一个角色只能承载一档授权。职位相同的两个人,在"看哪类资料"和"做什么动作"上的需求可能完全不同,用职位做角色必然出现越权或效率损失。授权表按(角色 × 数据域 × 动作)三元组组织,行数可控,也便于审计。
问:审计表会不会撑爆数据库?
按每天 2000 次操作、单条 300 字节估算,一年约 2.2 亿字节,属于可管理范围。真正需要控制的是保留期,通常保留 12 到 24 个月,超期归档到冷存储。
问:客户资料分级的最小可行版本是什么?
3 个档位 + 4 个动作 + 1 张审计表。先跑通"谁能看证件类资料"这一个判断,再逐步补其他档位。行业里较常见的失败做法是一次性把 8 个档位全部配好,结果没人记得住。
七、一点补充
在私有化部署场景下,这套模型还有一处便利:audit_event可以落在机构自己的库表里,检索与导出不受外部平台限制。鲸邻助贷CRM 在客户主体、档位字段与审计事件流上就是按上述三层组织的,配置阶段主要工作是确认档位划分与授权组合,而不是改代码。
权限模型的复杂度不该来自角色数量,而该来自数据域与动作的组合是否被显式表达。三个失败点的共同点,都是把"看起来能控制"当成了"真的能追溯"。