必须在应用层resultset取值后立即脱敏,而非dto组装或json序列化阶段,以防止日志、缓存等出口残留明文;需配置化掩码策略、分环境管控,并覆盖所有数据出口。

参数化查询能拦住注入,但拦不住合法用户查出明文敏感数据——掩码必须做,而且得在应用层 ResultSet 取值后立刻执行,不能依赖前端或日志配置。
ResultSet 取值后立即调用掩码工具
常见错误是把掩码逻辑放在 DTO 组装或 JSON 序列化阶段,导致日志、缓存、异常堆栈里仍残留明文。攻击者只要拿到一个已登录账号,就能绕过所有前端限制直接查库。
- 正确做法:从
ResultSet读出字段值的瞬间就脱敏,例如MaskUtils.maskIdCard(rs.getString("id_card")) - 避免重复掩码:先判断是否已脱敏,比如检查值是否含
"***"或符合掩码正则(如^\d{6}.*\d{4}$),否则可能把"110***1234"变成"110******4" - 别用
String.substring()直接截取:MySQL utf8mb4 下对 emoji 可能乱码,优先用StringUtils.substring()这类兼容 Unicode 的切片工具
数据库视图层掩码要显式写字段,禁用 SELECT *
PostgreSQL 视图里用 overlay() 比 concat(left(), '****', right()) 更高效,但函数本身不索引友好,且新增字段不会自动被掩码。
- 身份证掩码推荐:
overlay(id_card placing '****' from 9 for 4) - 手机号统一用:
concat(left(mobile,3), '****', right(mobile,4)),别用replace(mobile, substring(mobile,4,7), '****')—— MySQL 8.0+ 部分版本会报错 - 视图定义必须显式列出字段,
SELECT *是硬伤:一旦表加了新敏感列(如bank_account),视图里就漏掉了
掩码策略必须配置化且分环境生效
硬编码掩码规则(比如“身份证只留前后4位”)会导致合规审计失败,也阻碍灰度发布。测试环境关掩码方便排查没问题,但生产环境必须强制开启。
- 配置项示例:
mask.rule.id_card=front(6)+back(4),运行时加载,不改代码 - 生产配置必须带
prod前缀,如mask.rule.prod.id_card,防止本地配置误刷到线上 - 日志脱敏不能靠重写
toString():哪怕只是log.info("user: {}", user),也得拦截序列化过程,走统一脱敏入口
缓存里的敏感字段最容易被忽略
Redis 缓存的是脱敏后的结果还是原始实体?如果缓存的是未脱敏对象,那所有接口——包括导出、报表、内部调试——都可能把明文重新吐出来。
- 缓存前必须走同一套掩码逻辑,和 HTTP 响应路径保持一致
- 特别注意缓存穿透场景:空结果没缓存,但攻击者反复请求不存在的 ID,可能触发原始 SQL 查询并记录明文日志
- ORM 二级缓存(如 MyBatis L2Cache)也要确认是否启用了字段级脱敏插件,否则
select * from user查出来的对象直接进缓存
最麻烦的不是写掩码逻辑,而是确保它出现在每一个数据出口:ResultSet、日志、缓存、异步消息、导出文件、甚至数据库备份脚本里的临时表——漏掉任意一环,前面所有防护都白搭。











