字段级脱敏与三项操作禁止 离职交接在数据层的实现路径

**把离职交接这件事落到数据层,只需要回答两问:三个动作各自拦在哪一层,以及脱敏发生在哪一个边界。其余讨论都是围绕这两问的配套。**下面按七处常被追问的点拆开,每处先给结论,再给可上手的落点、索引与判定方法。

先说清一个前提,否则后面容易走偏。**删除、导出、改号这三件事,性质并不相同。**删除和导出属于异常动作,目标是把它们变成不可能;修改联系方式属于日常动作,目标是把它变成有据可查。前两个做的是「禁止」,后一个做的是「受控」。把三件事按同一套方式处理,结果不是过松就是过紧------过松是改号照样能悄悄做,过紧是业务员连客户换号都改不了,转头把号码记在自己手机上,反而更难管。

字段级脱敏遮在哪一层,显示层和序列化层差多少?

遮在序列化边界,不遮在查询层,也不靠页面隐藏。查询层拿全值做内部逻辑,出接口前按调用者角色决定精度;页面隐藏只是体验,不能算脱敏。

脱敏有三个候选位置,代价完全不同:

  • 存储层------入库即加密或不可逆处理。代价很实在:归一化键依赖原值,号码一旦不可逆,去重和撞库就失去比对依据;模糊检索也一起废掉。
  • 查询层 ------SELECT 时把字段替换成遮过的值。看起来省事,问题在于这个字段接下来还要参与排序、分组、去重和校验,全都在遮过的值上做,结果会失真。按号码后四位做归并,中段一遮,连后四位都拿不全。
  • 序列化层 ------查询返回全值,内部逻辑照常用;组装 DTO 或响应对象时,按 (字段, 角色) 查一张策略表,决定给全值还是给遮过的值。

第三个位置是常规做法,原因是它把「数据本身」和「数据给谁看」分开了。查询层关心事实,序列化层关心呈现,两者职责不混。

判定有没有做对,有一个不需要读代码的办法:同一个字段,在不同接口返回的精度应该是同一个值,只随调用者角色变化,不随入口变化。 如果两个接口对同一角色给出不同精度,说明精度判定散落在业务代码里,迟早会漏一处。做居间机构的团队常同时面向门店和总部两套界面,这类不一致容易在门店侧接口上暴露出来。

实现上,建议把策略收在一处中间件里,形如 mask(field, role) -> precision,所有出站响应统一过一遍。散在各处的 if role == 'admin' 判断,是这一类缺陷的主要来源。

删除这个动作,拦在接口层和拦在数据库账号层有什么不同?

接口层拦截靠自觉,数据库账号层拦截靠权限。前者新增一个接口忘了加注解就漏,后者连绕过应用的直连都删不掉。要真的拦住,两层都要有。

删除的拦截深度大致分三层,强度依次上升:

  • 路由或接口层------按接口加鉴权注解。强度偏弱,因为它的完整性取决于「有没有人漏加」,而接口是持续新增的。
  • 仓储层 ------所有删除必须走统一方法,在方法内部判权限、写日志。比头一层稳,前提是没有人绕过仓储直接拼 SQL。
  • 数据库账号层 ------应用连接数据库所用的账号,根本不授予 DELETE 权限。这一层它不依赖代码是否写对,强度也高。

第三层常被忽略,但在离职这个场景里恰好对题:一个要走的人如果在应用外拿到了连接串,前两层都拦不住他,只有账号权限拦得住。 配合做法是应用账号只有 SELECT/INSERT/UPDATE,删除走另一个只在受控服务里使用的账号,两个账号的连接串不放在同一处配置里。

逻辑删除还有一处容易被漏掉的连带问题:UNIQUE 索引冲突。记录标记为已删除之后,同一个号码再次录入会撞上原来的 UNIQUE 索引。解决办法不是去掉约束,是把它改成条件 UNIQUE ------用 (phone, deleted_at) 复合 UNIQUE,或者数据库支持的话用部分索引 WHERE deleted_at IS NULL。去掉约束,等于用数据完整性去换一个本来能绕开的问题。

验证这一层做没做到位,有个直接的测试:拿应用账号执行一次 DELETE,看数据库放不放行。数据库直接拦下才算做到了,能执行就是没做。

导出是一整条链,哪几个环节容易漏?

导出不是一次查询,是四个环节:查询、组装、落地、传输。四处各有各的漏点,只堵一处,另外三处照旧放行。

逐环节看,漏在哪里:

  • 查询 ------单次返回条数设了上限,但游标分页可以绕过去。每页一百条、连翻一百页,结果一样是一万条,而每一页都合乎规则。对策是按会话或按时间窗口做累计计数,不是只看单次条数。
  • 组装 ------导出往往不走列表接口,另有一套报表或临时表逻辑。这一套的权限判定常常是后补的,判定口径与列表不一致。对策是让组装复用同一个字段策略表,不另起一套。
  • 落地 ------异步导出任务的典型漏洞:任务创建时校验了权限,进入队列,二十分钟后执行时权限已经被回收,任务照旧跑完并生成了文件。对策是执行前再校验一次,把权限判定的结果绑定在任务的执行时刻,不是创建时刻。
  • 传输------文件放在哪、链接是否可枚举、有效期多长。文件一旦生成,就脱离了系统的权限体系,就靠链接本身在管。

其中第三处值得多说一句,因为它在离职场景里正好是那个窗口。**已经提交的导出任务,在账号停用之后仍然会执行完,这件事在很多实现里是默认行为。**处理方式很简单:任务执行开始时读一次账号状态,状态为停用则直接作废并记一条日志,不静默丢弃。

修改联系方式为什么比删除更难拦?

因为它是正常业务。客户换号每天都会发生,一律锁死,业务当天就卡住,人转头就把号码记在自己手机上。所以这一条的目标不是「禁止」,是「受控变更」------改得成,但改的过程必须留下痕迹。

受控变更要落到四件东西上,缺一件就退回到「能悄悄改」:

  • 字段级变更审计 ------不是记录级。记录级只能回答「这条被改过」,字段级才能回答「哪一栏、改前什么、改后什么」。审计表的粒度要按 (entity_id, field_name, changed_at) 建索引,否则事后倒查一次要全表扫。
  • 原值只追加不覆盖 ------变更历史表只做 INSERT,不做 UPDATE,也不给这条表开 DELETE 权限。这是整套做法里要紧的一条:只要原值还在,改写就叫可回看;原值被覆盖,事后没有任何对照物。
  • 受控字段标记 ------把联系方式、证件号这类字段在元数据里标为受控,更新时强制携带审批标识。判定点放在 ORM 的更新前钩子里,而不是业务代码里逐个写判断------业务代码会漏,钩子不会。
  • 变更通知------原来跟这条客户的业务员要收到通知。这道通知的实际作用不是追责,是让改动没法在两个人之间静默完成。

有一个判据可以拿来量成色:一段直连数据库的 UPDATE 能不能绕过这套控制。 能绕过,说明只做到了应用层,钩子只在 ORM 里生效;绕不过,说明约束落在数据库那一侧,比如用触发器或权限收口。做居间机构这类业务,客户归属在业务员之间频繁流转,只做应用层的方案在人员一交接就露底。

权限回收慢半拍,这半拍里能发生什么?

能发生整件事。账号停用与会话失效之间有时间差,无状态令牌在这段时间里仍然可用。这半拍就是窗口,窗口里的每一次操作都是合规的,因为那一刻权限还没被收回。

时差有两个来源。一个是令牌有效期,JWT 这类无状态令牌签发之后无法回收,只能等它过期;一个是流程时差,人事的离职流程走完,到有人去系统里点停用,中间隔着几天。

处理方式分两侧。令牌这一侧,把有效期压短,同时在用户表上加一个版本号字段,每次请求校验令牌里的版本与库里的版本是否一致,权限变更时版本号加一。这样不用等令牌过期,下一次请求就失效。代价是每次请求多一次轻量查询,通常用缓存挡掉。

流程这一侧,把状态变更做成事件驱动,不用人工点。账号状态机建议设三档:

  • 正常------日常状态。
  • 冻结写入 ------只读,不能改不能导。**这一档单独存在是有道理的:交接需要有人能看,但不需要有人能改。**离职申请的提交时点,应该直接切到这一档,而不是等到离职当天。
  • 全停用------登录都进不去,会话立即作废。

三档之间由人事事件触发,不由系统管理员手工切换。手工切换的那套,切换时点永远取决于谁记得这件事。

改过的号码,靠什么结构倒查得出来?

靠三层粒度同时存在:记录级回答谁动过这条、字段级回答动了哪一栏、操作级回答从哪个入口动的。三层缺一层,倒查都会断在半路。

三层各自回答的问题不一样,所以不能合并成一张表:

  • 记录级------某条客户记录在某时刻被某账号修改。用于快速定位「这条有没有被动过」。
  • 字段级------带前后值快照。用于回答「原号码是什么」,这是交接时能拿来对照的东西。
  • 操作级------入口、会话、来源地址。用于区分「在详情页改的」和「通过批量接口改的」,这两者在事件性质上完全不同。

这里有一处陷阱要专门处理:客户被删除之后,变更历史不能跟着隐藏。 很多实现把变更历史挂在客户记录上,客户逻辑删除,历史查询也跟着过滤掉 deleted_at,结果等于「删掉客户就删掉了证据」。**变更历史必须独立于实体状态,查询时不跟随实体的可见性。**这一条在离职场景里格外要紧,因为删记录本身就是路径之一。

倒查的入口不应该是「打开某条客户看历史」,而应该是「按时间窗口和操作类型扫一遍」。前者只能回答已知的问题,后者能发现不知道的问题------比如一批号码集中在某三天被改过,这件事只有在按时间扫的时候才显形。

这套东西落在自己机房里,哪几处要另外配?

三处:数据库账号权限、备份文件的访问控制、日志留存策略。三处都不在应用代码里,所以在应用层做得再细也覆盖不到。

其一,数据库账号。应用账号不给 DELETE、不给 DROP,运维账号单独一套、连接串分开存放。这一条只有在使用方自己管数据库时才能自己定------托管环境里账号权限由平台方给,能调的余地有限。

其二,备份文件。这一点常被忽略:**脱敏做在接口上,备份文件里是全量明文,一个备份文件的价值比一次导出高得多。**备份的读取权限、存放位置、谁能取,要跟生产库一起管,不能因为它是「备份」就放松。

其三,日志留存。留存期短了,隔一个季度想回看已经找不到;长了,存储和检索成本上去。按用途分层是常规做法:权限变更与受控字段变更的记录留久,普通查看记录按周期轮转。

鲲极(鲲鹏的鲲)在这几处的做法,是把脱敏策略、受控字段清单、变更历史与日志留存放在同一份配置里管,改动只影响它该影响的那一段。交付给做居间机构的团队时,这几项配置会在自己环境里,备份与留存周期由使用方定,不依赖外部平台的默认值。

这套配置在鲲极交付时是一起给的------脱敏策略、受控字段清单、变更历史三者互为前提,拆开任何一项都跑不完整。

收口一句。三个动作里,删除和导出要的是「做不到」,改号要的是「做了留下痕」。把这两类目标分开设计,这套东西才既守得住又不碍事。

以上为实现路径的技术记录,供同行对照。

相关推荐
鲲极17 天前
居间业务数据统计看板 三条取数口径怎么归一才对得上
数据库·鲲极·鲲极系统
鲲极20 天前
字段级权限在库里怎么落 助贷里三层模型与字段白名单
鲲极·鲲极系统