
接敏感信息处理的需求接多了会发现一个问题每个项目都在重复造轮子。日志脱敏要写接口返回值脱敏要写数据库表里的存量数据也要处理身份证、手机号、银行卡、地址、姓名各写一遍正则最后还漏得厉害。我后来把这类需求收敛成一个敏感信息工具类本质上就是一套可复用的检测加脱敏组件输入任意文本或结构化数据输出已经处理干净的结果。这篇文章把它的设计思路、核心实现和生产里踩过的坑一起梳理出来给正在做数据安全改造的团队一个可以直接参考的版本。1. 需求拆解敏感信息检测到底在解决什么问题1.1 先分清哪些数据算敏感信息很多公司做数据安全第一反应就是“把所有字段都加密”。这个想法很危险因为加密之后查询、统计、模糊搜索全废了业务方根本没法用。真正合理的方式是分级分类只有需要严格保护的那部分字段才做加密其余字段以脱敏为主。所以工具类要做的第一件事就是把“敏感信息”的清单定义清楚。常见的敏感信息大概分这么几类个人身份类身份证号、护照号、驾照号、港澳通行证号、统一社会信用代码里的法人身份证信息联系方式类手机号、座机号、邮箱、详细地址、紧急联系人信息金融账户类银行卡号、支付账号、银行卡CVV、第三方支付ID账号密码类登录名、口令、Token、AccessKey、私钥、数据库连接串里的密码生物特征类人脸底图、指纹特征、声纹数据、虹膜信息实际项目里我建议不要一上来就追求全量识别。先用最容易出问题的四类落地身份证、手机号、银行卡、邮箱。这四类字段格式相对固定检测难度低出现频率又高最容易踩雷。姓名、地址这类非结构化文本虽然也要处理但更依赖上下文后面单独说。这里有一个容易被忽略的场景工单系统。客服排查问题时经常把包含手机号、订单号、身份证号的截图或者文本直接贴到工单里而工单又是跨部门流转的。很多公司内部数据泄露就是从这个口子出去的。工具类除了服务代码还应该提供一个通用入口让客服系统、工单系统、企业IM机器人可以随时调用。另一个场景是日志平台。应用打印日志时如果没做处理身份证号和银行卡号会跟着异常堆栈一起落到日志里面日志一存就是半年一年被谁查过都说不清楚。所以日志侧接入工具类比接口返回脱敏还要优先级高。我的建议是先把日志链路打通再处理数据库和接口。1.2 工具类的职责边界敏感信息工具类看起来功能简单但实际边界很容易被人为扩大。有人希望它还能做数据分级、权限管控、审计留痕有人则希望它只做一个脱敏函数。我的经验是工具类只负责“识别”和“转换”其他事情全部交出去。具体来说识别层做两件事一是定位敏感信息出现在文本的哪些位置二是判断它的类型。转换层做一件事把定位出来的内容按照指定策略替换掉。至于谁来调用、调用时输入什么格式、转换结果存到哪里那是上层系统的事。边界清楚以后工具类才能保持稳定测试也好写。这个边界设计有个附带的好处它天然适合作为中间件接入任意技术栈。Java服务可以依赖一个SDKPython脚本也可以调同一套方案日志采集端可以直接内嵌。下面我讲的实现会以一个可嵌入的类库形态为例不限定具体框架。还有个很容易被忽略的点工具类必须无状态。这意味着它内部不能保存业务上下文不能依赖调用方的Session信息更不能把上一次处理的结果带到下一次处理里。无状态设计让它可以被任意并发调用也能在多个服务节点间水平扩展。2. 整体方案设计与技术选型2.1 检测技术路线正则先做人模型再兜底选型时最大的分歧往往在“用规则还是用模型”。规则派认为正则是工业界最稳的方案模型派认为自然语言里的敏感信息根本躲不开NER。我实际用下来的结论是两者不是替代关系而是串联关系。正则适合处理格式高度固定的信息。身份证号18位最后一位可能是数字或X手机号11位以1开头银行卡号13到19位基本都是纯数字。这些用正则就够了又准又快还能直接拿到命中位置。但正则的弱点也很明显。一段文本“王小明住在本市幸福路88号”你很难用正则判断“王小明”是不是姓名、地址是否完整。这时候需要姓名词典加启发式规则或者在隐私预算允许的情况下引入命名实体识别模型。我给出的推荐组合是第一层正则检测覆盖身份证、手机号、银行卡、护照号、邮箱、URL中的Token第二层规则补全比如手机号前加“86”、身份证号前后文出现“身份证号”字样通过上下文权重确认第三层实体模型用预训练的NER识别姓名、地址、机构名并对置信度低的候选结果只告警不脱敏这套组合的定位是“准度优先少量漏报可以接受但误报要少”。因为在生产环境里误脱敏会破坏业务数据比漏报更麻烦。具体到正则编写我一般会在匹配前后都加上边界断言。比如手机号规则(?!\d)1[3-9]\d{9}(?!\d)这个写法比裸写1[3-9]\d{9}多了前向和后向断言能避免把一段12位数字的中间11位误命中。身份证号更讲究日期范围必须限定(?!\d)[1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])\d{3}[\dXx](?!\d)日期限定是降低误报最有效的一步。很多初版工具把正则写成“18位数字加X结尾”结果把订单号、流水号、文件编号全部误伤。加了日期校验之后误报率能下降一个数量级。2.2 脱敏策略遮蔽、哈希还是加密识别之后怎么处理也是关键问题。我归纳下来主要有四类策略各自适用场景不同策略实现方式可逆性适合场景注意事项遮蔽用*替换中间部分不可逆日志展示、客服查看保留前后各几位便于业务判断哈希对原值做SHA256后取片段单向用户ID关联分析加盐防彩虹表但无法格式保持伪装用随机的假数据替换不可逆测试环境、演示环境需要保证业务关联性加密AES等对称加密可逆数据库存储、跨系统传输密钥管理要单独设计日志平台一般用遮蔽测试环境用伪装数据库存量改造用加密数据仓库做关联分析用哈希。一个工具类设计到位应该同时支持这几种策略具体用哪种由调用方在配置里指定。关于策略选择有个很容易踩坑的点不要把所有字段都做同样的处理。银行卡号和身份证号在日志里留前3后4没问题姓名如果留前1后1配合年龄段很容易反推出完整信息所以姓名我一般建议全遮蔽。这个细节必须在策略配置里做差异化。伪装策略还有一层讲究生成的假数据要和真实数据的格式保持一致。手机号伪装出来还得是11位身份证号还得符合校验规则否则下游系统在做格式校验时会直接报错。网上有几个开源库做这个做得不错但工具类里最好自己实现一个可插拔的生成器方便接企业自己的业务规则。2.3 工具架构规则引擎加处理器管线工具类的整体架构我建议拆成三层规则配置层定义敏感信息类型、正则表达式、脱敏策略、白名单检测执行层加载规则逐条匹配输出命中列表处理输出层根据命中列表和策略生成处理结果这三层对应代码里就是三个模块互相只依赖接口。这样做的好处是可测试性好新加一类敏感信息只需要改配置不需要动主流程。实际开发里我会把整个流程做成一条管线输入原始文本经过格式规范化、规则检测、模型补充、策略处理四个阶段最后输出处理后的文本。管线模式的优势是日志和链路追踪好做出现问题可以直接定位到具体阶段。格式规范化阶段经常被人忽略。后端接口接收到的文本可能带着全角符号、不间断空格、零宽字符这些都会干扰正则边界判断。我在规范化的处理顺序是全角转半角、Windows换行转LF、去掉首尾多余空白、保留合法内容。注意规范化只影响检测和脱敏不能改变最终存库的原始数据。3. 动手实现一个敏感信息检测与脱敏工具类3.1 数据模型与规则定义以Java为例所有规则都对应一个敏感信息类型定义public class SensitiveRule { private String type; // 类型编码如 ID_CARD、MOBILE private Pattern pattern; // 主匹配正则 private Policy policy; // 脱敏策略 private String mask; // 遮蔽串默认* private int prefixKeep; // 保留前几位 private int suffixKeep; // 保留后几位 private ListString keywords; // 上下文关键词用于二次确认 }规则统一放配置文件避免散落在代码里。我习惯用JSON或者YAML维护规则集启动时加载进内存同时支持远程配置中心推送更新。规则文件里每一项都要有enabled开关玄机在于灰度发布先关掉新规则等样本回归测试跑过了再打开。下面是一段规则配置示例- type: MOBILE pattern: (?!\\d)1[3-9]\\d{9}(?!\\d) policy: MASK mask: * prefixKeep: 3 suffixKeep: 4 keywords: [手机, 电话, contact, mobile] - type: ID_CARD pattern: (?!\\d)[1-9]\\d{5}(?:18|19|20)\\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx](?!\\d) policy: MASK mask: * prefixKeep: 1 suffixKeep: 1 keywords: [身份证, 证件号, id card, identity]这里的keywords是给上下文确认用的。如果文本里出现“手机13812345678”正则命中了手机号同时前后文有“手机”这个关键词置信度进一步提高。这算是简单版的上下文增强。3.2 核心检测实现核心检测不建议用“一次正则全量替换”的方式因为后续如果要做上下文确认你得先知道命中的位置。所以我的实现分两步先找位置再做决策。public ListHit detect(String text) { ListHit hits new ArrayList(); for (SensitiveRule rule : enabledRules) { Matcher matcher rule.getPattern().matcher(text); while (matcher.find()) { Hit hit new Hit(rule, matcher.start(), matcher.end(), matcher.group()); if (confirm(rule, text, hit)) { hits.add(hit); } } } return mergeAndSort(hits); }confirm这一步很关键。它做四件事一是检查命中内容是否在白名单里二是检查命中内容前后文是否包含类型关键词三是检查文本是否已处于“跳过处理”的标记区间四是处理区间重叠。重叠情况很容易发生比如一段文本同时满足“手机号加姓名”和“地址加手机号”两个规则这时候按优先级最高的规则处理。处理阶段我用策略模式实现public String process(String text, ListHit hits) { StringBuilder sb new StringBuilder(text); ListHit sortedHits hits.stream() .sorted(Comparator.comparingInt(Hit::getStart).reversed()) .collect(Collectors.toList()); for (Hit hit : sortedHits) { String masked hit.getRule().getPolicy().apply(hit, hit.getRaw()); sb.replace(hit.getStart(), hit.getEnd(), masked); } return sb.toString(); }倒序替换是个很实用的小技巧。如果正序替换前一次替换会改变后续命中的位置索引导致错位。倒序从后往前改前面索引永远不变逻辑就简单了。这个细节在面试里也常被问到其实就是考察对字符串可变性的理解。从外部看工具类对外只需要暴露两个方法detect(text)返回命中详情process(text)返回脱敏结果。如果业务方需要知道哪些位置被处理了用detect如果只是想把日志打干净直接process就行。3.3 策略配置与上下文补全遮蔽策略的代码没那么复杂但要考虑边界条件Override public String apply(Hit hit, String raw) { int len raw.length(); int keepPrefix Math.min(hit.getRule().getPrefixKeep(), len); int keepSuffix Math.min(hit.getRule().getSuffixKeep(), len); if (keepPrefix keepSuffix len) { return raw; // 长度太短不处理避免把整段遮完 } StringBuilder masked new StringBuilder(); masked.append(raw, 0, keepPrefix); for (int i keepPrefix; i len - keepSuffix; i) { masked.append(hit.getRule().getMask()); } masked.append(raw.substring(len - keepSuffix)); return masked.toString(); }这里有一个容易被忽略的细节保留位数不能写成固定值而是要根据原始长度动态计算。手机号可以保留前3后4但身份证号可以保留前1后1或前3后3一种策略对应一套规则不能搞成全局统一。上下文补全属于规则层面的事。比如在文本里检测到连续11位数字“13812345678”但前后文出现“开户手机号”这种关键词那么即使正则里已经加了边界判断我们也可以把置信度调高。反过来如果前后文出现“测试数据”“示例号码”应该直接跳过。白名单机制非常实用比如“10086”“400-800”这类号码就不用处理。为了应对更复杂的场景我会在工具里预留一个NER接口。默认实现可以是空操作但如果你引入spaCy、HanLP这类开源库或者自训练模型只要实现同一个接口就能接入。我的建议是先用规则版本跑通业务再去迭代模型不要一开始就把模型挂在主链路上。4. 实际落地中的常见问题与排查技巧4.1 误报和漏报的平衡怎么把握这个问题几乎每次评审都会被问到。简单回答不要追求100%覆盖要追求“该处理的都处理了不该处理的没动”。我经历过一次线上事故原因就是正则写得太宽。当时为了识别“可能存在的手机号”正则写成了\d{11}结果把一批订单号当成手机号给遮蔽了业务方查不到数据直接炸了。从那以后我定了一个规矩任何规则上线前必须用三类样本评估——正样本肯定要处理的、负样本肯定不能处理的、边界样本模棱两可的。正则宁可短一点、准一点不要贪多。漏报的问题一般出现在非结构化文本里。名字、地址、公司名这类信息没有固定格式正则几乎无能为力。处理手段是分级对模型识别出的实体做“告警但不替换”的配置让下游业务根据效果决定是否启用。这样既不会误伤数据也不会让敏感信息彻底裸奔。还有一个实际经验优先级管理要重视。如果文本同时包含姓名和身份证号比如“张伟身份证号110101199001011234”工具类检测到姓名“张伟”和身份证号两个命中但如果先替换姓名再替换身份证倒序替换仍能正确处理区间关键在于合并排序时不能让错误规则覆盖正确的处理区间。我的做法是身份证、银行卡这类高置信规则优先级最高姓名、地址这类低置信规则优先级最低。4.2 编码和中文场景的坑很多人写中文敏感信息规则时直接在正则里写中文字符看起来没问题实际会遇到全角半角、不可见字符、零宽断言这些坑。比如手机号前后可能出现全角空格、制表符、换行符。如果在序号和手机号之间有全角符号边界断言(?!\d)是拦不住的。我的做法是进入检测前先做一次文本规范化全角转半角、多余空白符去掉、统一换行符。规范化属于预处理阶段不应该影响业务展示用的原始文本。还有一个特别容易忽略的点识别文本里的中文身份证号。旧一代身份证是15位新一代是18位有些人录入时会在末尾多加一个空格。规则里可以允许末尾有少量空白但注意不能把下一位合法数字也吃进去。这类边界问题只有靠真实数据回放来发现纯靠人肉看正则很难全看出来。关于邮箱有个冷知识邮箱用户名部分其实可以包含很多特殊字符比如user.nametagdomain.com而且域名部分可以是中文域名。很多工具类的邮箱正则写得太简单漏掉一堆合法格式。我一般把邮箱划分为两个正则分别匹配用户名和域名减少漏报。4.3 批量场景下的性能优化要点工具类如果只在单条日志里跑性能无所谓。但它经常要接“全量导出数据清洗”这种批量任务性能就会成为瓶颈。我的优化经验集中在三块编译预加载、并行检测、避免重复计算。正则Pattern必须在初始化时全部编译不能在每条数据处理时重新编译。很多人没注意这一点写了个方法内部直接Pattern.compile几百万条数据跑下来耗时能差出几倍。另外检测过程本身是无状态的完全可以按文本长度分区多个线程并行处理。我用的是JDK自带的并行流配置好线程池后压测过8核机器上性能能提升四五倍。重复计算主要发生在正则和模型同时存在时。正则已经命中并确认过的位置模型再跑一遍就是浪费。我会在管线里加一个去重层把已经确认过的区间传给下一阶段模型只处理未覆盖的区域。这一步对性能提升很大尤其是正则能命中大量高频类型时。还有一个小技巧如果工具类要处理海量短文本可以加一个“预过滤”逻辑。先用一个轻量级的宽松正则判断文本里是否含有可能的敏感信息比如是否包含数字或者常见敏感关键词。如果完全不包含就直接跳过后续复杂检测。预过滤的正则非常快能把90%不需要处理的文本挡在外面。4.4 上线前的验证手段和样本库维护工具类的测试不能只靠单元测试里的几个用例必须建一个真实样本库。每次在业务日志里收集被误报或漏报的样本累计维护成回归集。工具类升级任何一个正则或者模型先全部跑一遍回归集看有没有新引入的变更。我常用的方法是给每次版本迭代做一次对比报告上一版本和本版本对同一份样本库的输出差异。差异太大说明风险高需要人工核对。这个流程看起来重实际上很值得做因为在敏感信息处理上最不能接受的就是“之前处理的这次没处理到”。回归样本库的分类可以这样组织样本类型来源用途正样本真实日志、真实工单、构造样例验证规则能命中负样本订单号、流水号、数字编号验证规则不误报边界样本全角符号、换行干扰、伪装号码验证规则的边界处理能力回归样本历次问题复现数据防止历史问题复发样本库要落到代码仓库里版本跟着工具类走。每修复一个线上问题必须补一条回归样本确保下次不会在同一条路上摔第二次。这也是我团队内部约定俗成的规矩。5. 个人实操经验与后续演进方向做了三四个项目的敏感信息工具类重构之后我自己的判断是这类工具真正难的地方不是写代码而是“规则生命周期管理”。业务变化快新的敏感信息类型不断出现老规则也可能因为业务格式调整而失效。工具类必须支持热更新配置而且最好有一个管理端界面让非技术人员也能维护白名单和规则。另外把工具类做成插件形态是值得考虑的。比如接入到日志采集端数据在落盘前就完成脱敏接入到数据库代理查询结果返回前自动改写接入到消息队列生产端直接消费处理后的数据。这些都是把同一个检测引擎复用到更多场景的方式。我在实际维护过程中还有一个体会不要停止收集反例。表面上看起来规则已经很完善但真实业务里总会出现让正则措手不及的数据。把每一个反例记下来、归类、补进回归集工具类才会越来越稳。最后分享一个小技巧上线时保留一段时间的“双写对比”。重构完的检测引擎和老方案同时跑对比输出差异。等差异收敛到零再切换流量。这一步看似保守却是经历过线上事故的人都会选择的做法。一开始可能觉得多此一举但等你真正切完流量之后才会明白这个动作给全团队省下的不是一点半点心力。