data-agent

敏感字段如何脱敏

前面讲的数据权限,解决的是当前用户能查到哪些行。但有些字段即使在权限范围内,也不应该直接展示,比如密码、身份证号、家庭住址。 所以 executeSql 在查询成功后,还会做一次敏感字段脱敏。数据权限是在 SQL 执行前过滤行,敏感…

TL;DR

前面讲的数据权限,解决的是当前用户能查到哪些行。但有些字段即使在权限范围内,也不应该直接展示,比如密码、身份证号、家庭住址。 所以 executeSql 在查询成功后,还会做一次敏感字段脱敏。数据权限是在 SQL 执行前过滤行,敏感…

前面讲的数据权限,解决的是当前用户能查到哪些行。但有些字段即使在权限范围内,也不应该直接展示,比如密码、身份证号、家庭住址。 所以 executeSql 在查询成功后,还会做一次敏感字段脱敏。数据权限是在 SQL 执行前过滤行,敏感字段脱敏是在 SQL 执行后处理列值,这两个能力解决的问题不一样。

整体流程

这条链路里,SensitiveFilter 的位置很靠后。SQL 已经校验过,数据权限也已经注入过,数据库也已经执行完了。它处理的是内存里的查询结果,把命中的敏感列替换成 **,再交给 format 渲染成 Markdown。

敏感字段配置

敏感字段是放在配置文件里:

data
  sensitive

这里有两个配置: - enabled:是否开启脱敏 - mask-fields:哪些字段需要脱敏 当前配置了三类字段:登录密码、身份证号、家庭住址。真实业务里可以按自己的表结构继续加,比如手机号、银行卡号、邮箱等。

配置怎么加载

SensitiveFilter 启动时会读取这些配置: 这里同时维护了两份集合: - sensitiveFields:完整字段,比如 sys_user.password、user_profile.id_card - sensitiveColumns:字段名,比如 password、id_card、home_address 为什么要存两份?可以从配置和查询结果两边理解。 配置里写的是 sys_user.password,它表达的是"只要来源是 sys_user 表的 password 字段,就要脱敏"。所以我们保留一份完整字段集合 sensitiveFields,后面拿到 JDBC 元数据时,可以用 sys_user.password 精确匹配。但查询结果展示时,列名不一定带表名,因为某些查询或数据库驱动不一定能稳定拿到表名,尤其是复杂表达式、函数列、子查询结果。这时候如果拼不出 sys_user.password,就退一步按字段名 password 判断。简单说:sensitiveFields 用来匹配完整规则,比如 sys_user.password;sensitiveColumns 是从配置里提取出来的字段名,比如 password,用于拿不到表名时兜底。

查询结果columnMetas

一开始最直接的实现,是只看查询结果的列名。比如返回列里有 password,就把这一列脱敏。 问题是 SQL 可以写别名:

SELECT

这时候结果表头是 pwd,不是 password。如果只按结果列名判断,就识别不到这个字段其实来自 sys_user.password。 所以 ReadOnlyQueryRunner 读取 ResultSet 时,除了保存展示列名 columns,还额外保存 columnMetas: getColumnLabel 是结果集最终展示出来的列名,写了别名就返回别名。getColumnName 是数据库真实字段名,getTableName 是字段来源表名。 比如:

SELECT

columns 是:

["id", "username", "pwd"]

columnMetas 是:

[
  ColumnMeta(label="id",       tableName="sys_user", columnName="id"),
  ColumnMeta(label="username", tableName="sys_user", columnName="username"),
  ColumnMeta(label="pwd",      tableName="sys_user", columnName="password")
]

这样 SensitiveFilter 就能知道:虽然展示列名叫 pwd,但真实字段是 sys_user.password,应该脱敏。

mask替换结果值

executeSql 查询成功后,会调用 SensitiveFilter: 真正的脱敏逻辑是:先找出命中的列,再把这些列的值替换成固定掩码。 这里替换的是 rows 里的值。也就是说,数据库查出来的原始值不会再进入后面的 Markdown 渲染结果。 脱敏前: | id | username | pwd | | --- | --- | --- | | 1 | admin | 123456 |

脱敏后: | id | username | pwd | | --- | --- | --- | | 1 | admin | ** |

怎么判断是不是敏感字段

判断逻辑分两层:优先按真实字段判断,兜底按展示列名判断。 isSensitiveField 负责判断真实字段: 比如 SELECT password AS pwd FROM sys_user: - label 是 pwd - tableName 是 sys_user - columnName 是 password 先判断 sys_user.password,命中配置,所以脱敏。这样就避免了简单别名绕过。最后的 sensitiveColumns.contains(col) 是兜底。有些数据库驱动或者复杂表达式可能拿不到 tableName,这时候还能按字段名判断一次。当前实现是全局脱敏:只要字段命中配置,不管是谁查,结果都返回 *。适合大多数"敏感字段默认不展示"的场景。如果业务中要做得更精细,可以直接结合现有角色体系判断。比如 admin 和部门主管可以看身份证号明文,DEPT 和 SELF 范围的普通角色不能看。实现上就是在 isSensitiveField 命中之后,再根据当前用户角色或 dataScope 判断一次:有明文查看权限就原样返回,没有权限就替换成 *

M-Schema 示例值不采样敏感字段

除了 executeSql 查询结果,还有一个容易忽略的地方:describeTables。 M-Schema 会采集字符串字段的示例值,帮助 LLM 理解字段内容。如果不处理,password、id_card、home_address 这类字段也可能被采样到示例值里。 所以在采样前加一层判断: 这样敏感字段不会进入 M-Schema 的 examples,describeTables 也就不会把这些示例值暴露给 LLM。 这个处理比采样后再替换更干净,因为敏感值从一开始就不进缓存。

✅M-Schema的Java实现

M-Schema 说白了就是读数据库元数据,按约定格式拼成一段 LLM 好读的文本。官方实现基于 Python 的 SQLAlchemy。 dodo-agentx 是 Java 项目,没必要为了生成一段 schema 描述,还要额外拉一个 LLMentor

用户上下文也要脱敏

UserContextBuilder 会把当前用户的身份、角色、部门、个人档案拼进 system prompt。这里也不能直接写敏感值。 原来个人档案里会直接写身份证号、家庭住址,现在写入前先过 SensitiveFilter: maskValue 的逻辑很简单:如果这个字段命中敏感配置,就返回 **,否则原样返回。 这样 system prompt 里也不会出现真实身份证号和家庭住址。

小结

敏感字段脱敏解决的是查到结果以后,哪些值不能明文展示。 当前实现覆盖了三条链路: - executeSql 结果脱敏:查询成功后,根据 columnMetas 判断真实字段来源,把敏感列替换成 ** - M-Schema 示例值保护:敏感字段不采样 examples,避免 describeTables 暴露样例 - 用户上下文脱敏:身份证号、家庭住址写入 system prompt 前先脱敏 这里最关键的优化是 columnMetas。只看结果列名会被别名影响,而 columnMetas 保存了展示列名(别名)和真实字段来源之间的关系,所以 password AS pwd 也能正确脱敏。

版本提示

模型、框架与接口会持续变化。涉及版本号、参数与生产配置时,请在实践前对照对应官方文档。

LLMentor系统化学习大模型应用工程

内容来自个人课程知识库备份,并经过结构化整理。技术版本持续演进,生产使用前请结合官方文档验证。