敏感数据脱敏
发布时间:2026/9/28 21:22:32来源:尧图网络
顺手再加个敏感数据脱敏上一篇做完了字典翻译这套 SPI 还剩一个很划算的用途敏感数据脱敏。引擎是共用的这篇只需要写 5 个文件。没看过上一篇的话先把那四个json包的文件JsonValueEnhancer、JsonFieldProcessor、JsonFieldContext、JsonEnhancementContext和TranslationConfig装进项目。这篇只讲脱敏部分。问题接口返回{empName:张三,phone:13812345678,idCard:310101199003071234}手机号、身份证直接裸露。放前端脱敏没用——接口一调全出来了必须在后端处理。常见做法是每个 VO 里手动调一遍StringUtils.mask(phone)跟字典翻译面临一样的问题到处是重复代码还容易漏。我们想要的是{empName:张*,phone:138****5678,idCard:310101********1234}VO 上打个注解就行Sensitive(typeSensitiveType.PHONE)privateStringphone;依赖不用加一个都不用。完全复用上一篇的引擎也没有 Hutool 依赖脱敏算法自己实现。和字典翻译的区别搞清楚这三点代码就很好懂字典翻译脱敏要不要查外部数据要查字典不要纯字符串处理要不要批量预取要防 N1不要用不上 collect/prepare读哪个字段的值一般用mapper读别的字段就读自己这个字段不需要mapper所以这边只需要实现supports()和process()另外两个用接口的默认空实现。设计每个脱敏类型自带一个函数不要写成一个大switch——那样每加一种脱敏方式都要回来改处理器。用策略模式SensitiveStrategy 接口就一个方法FunctionString, String desensitizer() ▲ │ implements SensitiveType 枚举每个枚举项自带脱敏函数 SensitiveJsonFieldProcessor 处理器只管取注解然后 apply不管具体怎么脱加一种脱敏方式 加一个枚举项。处理器一行都不用改。代码包名cn.prohup.xiao.crm.commons.sensitivecommons/sensitive/ ├── annotation/ Sensitive.java ├── core/ │ ├── SensitiveStrategy.java 策略接口 │ ├── SensitiveType.java 枚举实现策略 │ └── util/ │ └── SensitiveUtil.java 脱敏小工具 ├── handler/ SensitiveJsonFieldProcessor.java └── config/ SensitiveConfig.java1. 注解annotation/Sensitive.javapackagecn.prohup.xiao.crm.commons.sensitive.annotation;importcn.prohup.xiao.crm.commons.sensitive.core.SensitiveType;importjava.lang.annotation.Documented;importjava.lang.annotation.ElementType;importjava.lang.annotation.Retention;importjava.lang.annotation.RetentionPolicy;importjava.lang.annotation.Target;/** * type 脱敏类型 * beginIndex CUSTOM 模式保留的前缀位数 * endIndex CUSTOM 模式保留的后缀位数 */Retention(RetentionPolicy.RUNTIME)Target({ElementType.FIELD,ElementType.METHOD})DocumentedpublicinterfaceSensitive{SensitiveTypetype()defaultSensitiveType.CUSTOM;intbeginIndex()default0;intendIndex()default0;}2. 策略接口和工具类core/SensitiveStrategy.javapackagecn.prohup.xiao.crm.commons.sensitive.core;importjava.util.function.Function;/** * 脱敏策略一个策略就是一个字符串转换函数入参原文出参脱敏后的文本。 * * 想自己扩展的话实现这个接口即可处理器不用动。 * 也可以自定义一个 Function 直接塞给 SensitiveType见 SensitiveType.CUSTOM 的写法。 */publicinterfaceSensitiveStrategy{/** 返回该策略对应的脱敏函数 */FunctionString,Stringdesensitizer();}core/util/SensitiveUtil.javapackagecn.prohup.xiao.crm.commons.sensitive.core.util;importjava.util.Arrays;publicfinalclassSensitiveUtil{privatestaticfinalcharMASK*;privateSensitiveUtil(){}/** * 保留前 prefix 位和后 suffix 位中间全部填 *。 * 位数按实际长度算不是固定几颗星。 */publicstaticStringkeep(Stringstr,intprefix,intsuffix){if(strnull||str.isEmpty()){returnstr;}intlenstr.length();// 短到前后都要塌陷的地步直接全遮if(lenprefixsuffix){returnrepeat(len);}StringBuildersbnewStringBuilder();sb.append(str.substring(0,prefix));sb.append(repeat(len-prefix-suffix));sb.append(str.substring(len-suffix));returnsb.toString();}/** 只留第一个字张三 → 张* */publicstaticStringname(Stringstr){if(strnull||str.length()1){returnstr;}returnstr.substring(0,1)repeat(str.length()-1);}/** 保留首位和 之后的完整域名zhangsan163.com → z*******163.com */publicstaticStringemail(Stringstr){if(strnull||str.isEmpty()){returnstr;}intatstr.indexOf();if(at0){returnkeep(str,1,0);}StringBuildersbnewStringBuilder();sb.append(str.substring(0,1));sb.append(repeat(at-1));sb.append(str.substring(at));returnsb.toString();}publicstaticStringrepeat(intcount){if(count0){return;}char[]arrnewchar[count];Arrays.fill(arr,MASK);returnnewString(arr);}}3. 枚举实现策略core/SensitiveType.java每个枚举项直接把自己对应的函数写进去。想加新的就加一行。packagecn.prohup.xiao.crm.commons.sensitive.core;importcn.prohup.xiao.crm.commons.sensitive.core.util.SensitiveUtil;importjava.util.function.Function;/** * 内置脱敏类型每个枚举项自带脱敏函数。 * 加一种脱敏方式 加一个枚举项处理器完全不用动。 */publicenumSensitiveTypeimplementsSensitiveStrategy{/** 手机138****5678保留前 3 后 4 */PHONE(s-SensitiveUtil.keep(s,3,4)),/** 身份证保留前 6 后 4 */ID_CARD(s-SensitiveUtil.keep(s,6,4)),/** 银行卡只留后 4 位 */BANK_CARD(s-SensitiveUtil.keep(s,0,4)),/** 地址保留前 6 个字 */ADDRESS(s-SensitiveUtil.keep(s,6,0)),/** 姓名只留第一个字 */NAME(SensitiveUtil::name),/** 邮箱保留首位和域名 */EMAIL(SensitiveUtil::email),/** 自定义位数由注解上的 beginIndex / endIndex 决定处理器里单独处理 */CUSTOM(s-s);/** 可自行添加其他脱敏策略比如护照 s - SensitiveUtil.keep(s, 2, 3) */privatefinalFunctionString,Stringdesensitizer;SensitiveType(FunctionString,Stringdesensitizer){this.desensitizerdesensitizer;}OverridepublicFunctionString,Stringdesensitizer(){returndesensitizer;}}CUSTOM为什么是s - s原样返回因为它的位数来自注解参数没法在枚举里预先定死得在处理器里特判。下面会看到。4. 处理器handler/SensitiveJsonFieldProcessor.javaOrder(10)是执行顺序数字越大越靠后。脱敏通常安排在翻译之后——万一某字段既翻译又要脱敏先拿到可读文本再脱敏。packagecn.prohup.xiao.crm.commons.sensitive.handler;importcn.prohup.xiao.crm.commons.sensitive.annotation.Sensitive;importcn.prohup.xiao.crm.commons.sensitive.core.SensitiveType;importcn.prohup.xiao.crm.commons.sensitive.core.util.SensitiveUtil;importcn.prohup.xiao.crm.commons.translation.json.JsonEnhancementContext;importcn.prohup.xiao.crm.commons.translation.json.JsonFieldContext;importcn.prohup.xiao.crm.commons.translation.json.JsonFieldProcessor;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.core.annotation.Order;/** * 敏感字段脱敏。不需要 collect / prepare纯字符串处理没有批量查询的必要。 * 具体的脱敏逻辑都挂在 SensitiveType 上这里只负责取注解、调函数。 */Order(10)publicclassSensitiveJsonFieldProcessorimplementsJsonFieldProcessor{privatestaticfinalLoggerlogLoggerFactory.getLogger(SensitiveJsonFieldProcessor.class);Overridepublicbooleansupports(JsonFieldContextfieldContext){returnfieldContext.getAnnotation(Sensitive.class)!null;}OverridepublicObjectprocess(JsonFieldContextfieldContext,Objectvalue,JsonEnhancementContextcontext){SensitivesensitivefieldContext.getAnnotation(Sensitive.class);if(sensitivenull){returnvalue;}// 只处理字符串数字、日期、对象一律放过if(!(valueinstanceofString)){returnvalue;}Stringstr(String)value;if(str.isEmpty()){returnstr;}try{SensitiveTypetypesensitive.type();// CUSTOM 的位数来自注解不能预先定成一个函数单独走这里if(typeSensitiveType.CUSTOM){returnSensitiveUtil.keep(str,sensitive.beginIndex(),sensitive.endIndex());}// 其余类型直接取枚举自带函数执行returntype.desensitizer().apply(str);}catch(Exceptione){// 脱敏出错宁可返回原文也不能让整个接口挂掉log.error(脱敏异常 field{},fieldContext.propertyName(),e);returnvalue;}}}5. 装配config/SensitiveConfig.java只需要注册处理器 Bean。引擎不用再注册一遍——上一篇TranslationConfig里的JsonValueEnhancer会自动把容器里所有JsonFieldProcessor收集进去脱敏这个自然挂上去了。packagecn.prohup.xiao.crm.commons.sensitive.config;importcn.prohup.xiao.crm.commons.sensitive.handler.SensitiveJsonFieldProcessor;importorg.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;ConfigurationpublicclassSensitiveConfig{/** * 引擎JsonValueEnhancer在上一篇的 TranslationConfig 里已经注册好了 * 它通过 Listlt;JsonFieldProcessorgt; 自动收集所有处理器这里只要把自己交给 Spring 即可。 */BeanConditionalOnMissingBeanpublicSensitiveJsonFieldProcessorsensitiveJsonFieldProcessor(){returnnewSensitiveJsonFieldProcessor();}}跑起来VO 加注解DatapublicclassEmployeeVo{privateLongempId;Sensitive(typeSensitiveType.NAME)privateStringempName;Sensitive(typeSensitiveType.PHONE)privateStringphone;Sensitive(typeSensitiveType.ID_CARD)privateStringidCard;Sensitive(typeSensitiveType.EMAIL)privateStringemail;Sensitive(typeSensitiveType.BANK_CARD)privateStringbankCard;/** 保留前 2 后 2中间全遮 */Sensitive(typeSensitiveType.CUSTOM,beginIndex2,endIndex2)privateStringremark;}拿这组数据跑张三/13812345678/310101199003071234zhangsan163.com/6222020200011234/客户沟通很满意实际返回这组值是真跑出来的不是手写示意{empId:1,empName:张*,phone:138****5678,idCard:310101********1234,email:z*******163.com,bankCard:************1234,remark:客户***满意}注意remark是 7 个字客户沟通很满意保留前 2 后 2中间 3 个字全遮——keep()按实际长度算位数不是固定几颗星。empId是 Long 没被处理符合只处理字符串。列表、嵌套对象、MapString, Object返回值统统生效因为递归是引擎做的。某个接口想看明文比如管理员才可见的详情页别用带注解的 VO另定义一个不加密注解的 VO 返回即可。加一种脱敏策略只要两步都在这个包里处理器不用动。第一步SensitiveUtil里如果没有现成方法加一个/** 护照保留前 2 后 3 */publicstaticStringpassport(Stringstr){returnkeep(str,2,3);}keep已经能覆盖大部分情况所以这步经常可以跳过。第二步枚举里加一项/** 护照保留前 2 后 3 */PASSPORT(s-SensitiveUtil.keep(s,2,3)),完事。VO 里就能用Sensitive(type SensitiveType.PASSPORT)了。一个容易踩的坑策略类直接用不了你可能会想单独写一个策略类publicclassPhoneTailStrategyimplementsSensitiveStrategy{OverridepublicFunctionString,Stringdesensitizer(){// 长度够 4 位4 颗星 后 4 位不够就全遮否则 substring 会越界returns-s.length()4?****s.substring(s.length()-4):SensitiveUtil.repeat(s.length());}}它单独调用是没问题的newPhoneTailStrategy().desensitizer().apply(13812345678);// ****5678但它接不到Sensitive注解上——注解里type()的类型是SensitiveType枚举填不进一个 classSensitive(type PhoneTailStrategy.class)编译不过。想让它生效最省事的办法是把 lambda 直接写成枚举项/** 手机尾号只显示后 4 位前面补 **** */PHONE_TAIL(s-s.length()4?****s.substring(s.length()-4):SensitiveUtil.repeat(s.length())),然后 VO 里就能用Sensitive(typeSensitiveType.PHONE_TAIL)privateStringphone;// 13812345678 → ****5678结论SensitiveStrategy接口的价值在于「枚举项里可以挂任意 lambda」而不是让你在外面 new 一个策略类。真要做到可插拔比如让第三方 jar 也能加策略注解得改成Class? extends SensitiveStrategy strategy()、处理器里再反射实例化——复杂度上去了一般业务用不上枚举项足够。坑现象原因脱敏没生效只处理 String。数字、日期、Long 不会脱敏需要脱敏的类型得先转成 String报非法 rangebeginIndex endIndex超过字符串长度keep()里已做全遮兜底和字典翻译互相覆盖同一字段别同时打两个注解打架。真需要的话顺序是翻译先Order(0)、脱敏后Order(10)加了注解但值是 null直接返回不报错导出 Excel 没脱敏本文只处理 JSON 响应导出走的是另一条路要在导出逻辑里单独处理和上一篇共用的验收列表 100 条字典查询日志只出现 1 次。加了脱敏之后这条还得成立——脱敏本身没有任何 IO如果变慢了说明处理器里混进了查询操作。两套能力加起来二十来个文件共用同一个引擎。想再加第三种比如给 OSS 的 fileId 自动补成完整访问 URL也是同一个套路写个实现JsonFieldProcessor的类supports()判断注解process()做转换注册成 Bean 就完事。
网站建设高端定制企业官网