电子商务有限公司怎么注册,网站内链优化,免费php域名网站,网络营销工具的特点在白嫖的时候#xff0c;希望你会内疚#xff0c;一键三连吧#xff0c;源码在最后#xff0c;自取在白嫖的时候#xff0c;希望你会内疚#xff0c;一键三连吧#xff0c;源码在最后#xff0c;自取在白嫖的时候#xff0c;希望你会内疚#xff0c;一键三连吧#…在白嫖的时候希望你会内疚一键三连吧源码在最后自取在白嫖的时候希望你会内疚一键三连吧源码在最后自取在白嫖的时候希望你会内疚一键三连吧源码在最后自取一、整体架构与设计思路核心目标实现「无侵入、可扩展、多类型」的数据脱敏支持字符串/日期/数值等类型适配Spring Boot接口返回场景满足合规要求如手机号、身份证、地址脱敏。技术选型AOP拦截方法返回值处理字符串类型字段脱敏Jackson序列化器处理Date类型字段脱敏避免类型赋值冲突注解驱动通过自定义注解标记需要脱敏的方法/字段灵活配置规则工具类解耦将脱敏规则封装为工具方法便于扩展和复用。核心流程接口请求 → 执行Sensitive注解方法 → AOP拦截返回值 → 递归处理对象/集合 → 字符串字段通过工具类脱敏 → Date字段通过Jackson序列化器脱敏 → 返回脱敏后数据二、代码模块逐行解析1. 枚举类SensitiveType脱敏类型定义public enum SensitiveType { PHONE, // 手机号138****1234 ID_CARD, // 身份证110101********1234 NAME, // 姓名张* PASSWORD, // 密码****** CUSTOM, // 自定义保留前后N位 ADDRESS, // 地址仅保留省市区 AMOUNT, // 金额全* TIME, // 时间全* ORDER_NO // 订单号最后6位* }作用标准化脱敏类型避免硬编码配合注解使用让脱敏规则可配置。扩展点新增脱敏类型时只需在枚举中添加再补充工具类方法即可。2. 注解类Sensitive方法级标记Target(ElementType.METHOD) // 仅作用于方法 Retention(RetentionPolicy.RUNTIME) // 运行时保留AOP可反射获取 Documented public interface Sensitive {}作用标记需要脱敏的接口方法AOP通过该注解作为切入点无需修改业务代码。使用场景Controller层接口方法上添加Sensitive即可自动脱敏返回值。3. 注解类SensitiveField字段级标记Target(ElementType.FIELD) // 仅作用于字段 Retention(RetentionPolicy.RUNTIME) Documented public interface SensitiveField { SensitiveType type(); // 脱敏类型必填 int prefixLen() default 2; // 自定义脱敏-前缀长度 int suffixLen() default 2; // 自定义脱敏-后缀长度 }作用标记实体类中需要脱敏的字段并指定脱敏规则类型自定义参数。使用示例// 手机号脱敏 SensitiveField(type SensitiveType.PHONE) private String phone; // 自定义脱敏保留前3后2 SensitiveField(type SensitiveType.CUSTOM, prefixLen 3, suffixLen 2) private String bankCard;4. AOP切面SensitiveAspect核心处理逻辑核心属性// 递归深度限制避免无限递归如对象循环引用 private static final int MAX_RECURSION_DEPTH 10; // 已脱敏对象缓存避免重复处理同一对象提升性能 private final ThreadLocalSetObject desensitizedCache ThreadLocal.withInitial(ConcurrentHashMap::newKeySet); // 字段缓存缓存类的字段信息避免重复反射提升性能 private final MapClass?, Field[] fieldCache new ConcurrentHashMap();重点ThreadLocal保证多线程安全每个线程独立缓存递归深度限制防止对象循环引用导致栈溢出字段缓存反射获取字段是耗时操作缓存后提升性能。切入点方法Pointcut(annotation(org.springblade.business.aspect.annotation.Sensitive)) public void sensitivePointcut() {}作用匹配所有添加Sensitive注解的方法作为AOP拦截入口。返回通知desensitize入口方法AfterReturning(value sensitivePointcut(), returning result) public void desensitize(JoinPoint joinPoint, Object result) { try { desensitizeObject(result, 0); // 递归处理返回值 } catch (Exception e) { log.error(脱敏切面:脱敏失败, e); } finally { // 清空缓存避免内存泄漏 desensitizedCache.get().clear(); desensitizedCache.remove(); } }作用方法执行完成后对返回值进行脱敏处理最终清空缓存。核心递归方法desensitizeObjectprivate void desensitizeObject(Object obj, int depth) { // 终止条件1对象为空 if (obj null) return; // 终止条件2递归深度超限 if (depth MAX_RECURSION_DEPTH) { log.warn(递归深度超过{}终止脱敏, MAX_RECURSION_DEPTH); return; } // 终止条件3对象已脱敏避免重复处理 if (desensitizedCache.get().contains(obj)) return; desensitizedCache.get().add(obj); // 标记已脱敏 // 适配RT返回格式SpringBlade通用返回对象 if (obj.getClass().getSimpleName().equals(R)) { Field dataField obj.getClass().getDeclaredField(data); dataField.setAccessible(true); desensitizeObject(dataField.get(obj), depth 1); return; } // 处理集合List/Set if (obj instanceof Collection? collection) { for (Object item : collection) desensitizeObject(item, depth 1); return; } // 处理数组 if (obj.getClass().isArray()) { Object[] array (Object[]) obj; for (Object item : array) desensitizeObject(item, depth 1); return; } // 处理单个业务对象 desensitizeSingleObject(obj, depth 1); }核心逻辑终止条件空对象、递归超限、已脱敏对象避免无效处理适配通用返回对象RT仅处理data属性中的业务数据兼容集合/数组遍历元素逐个脱敏单个对象交给desensitizeSingleObject处理字段级脱敏。字段级脱敏desensitizeSingleObjectprivate void desensitizeSingleObject(Object obj, int depth) { Class? clazz obj.getClass(); // 排除基础类型/String/DateDate交给序列化器处理 if (clazz.isPrimitive() || obj instanceof String || obj instanceof Date) return; Field[] fields getCachedFields(clazz); // 获取缓存的字段 for (Field field : fields) { field.setAccessible(true); Object fieldValue field.get(obj); Class? fieldType field.getType(); // 跳过Date类型序列化器处理 if (fieldType Date.class) continue; // 无脱敏注解递归处理子对象 if (!field.isAnnotationPresent(SensitiveField.class)) { if (fieldValue ! null !isExcludeType(fieldType)) { desensitizeObject(fieldValue, depth 1); } continue; } // 有注解仅处理字符串类型字段 if (fieldValue instanceof String strValue) { SensitiveField annotation field.getAnnotation(SensitiveField.class); String desensitizedStr getDesensitizedString(strValue, annotation.type(), annotation.prefixLen(), annotation.suffixLen()); field.set(obj, desensitizedStr); // 替换为脱敏后的值 } } }重点排除基础类型/Date基础类型无需脱敏Date交给Jackson序列化器跳过框架类型通过isExcludeType排除Spring/MyBatis等框架类避免反射异常仅处理字符串字段避免类型赋值冲突如数值类型直接脱敏会报错反射修改字段值通过field.set(obj, desensitizedStr)替换原始值。工具类调用getDesensitizedStringprivate String getDesensitizedString(String fieldValue, SensitiveType type, int prefixLen, int suffixLen) { if (fieldValue.isBlank()) return fieldValue; return switch (type) { case PHONE - DesensitizeUtil.desensitizePhone(fieldValue); case ID_CARD - DesensitizeUtil.desensitizeIdCard(fieldValue); case NAME - DesensitizeUtil.desensitizeName(fieldValue); case PASSWORD - DesensitizeUtil.desensitizePassword(fieldValue); case CUSTOM - DesensitizeUtil.desensitizeCustom(fieldValue, prefixLen, suffixLen); case ADDRESS - DesensitizeUtil.desensitizeAddress(fieldValue); case AMOUNT - DesensitizeUtil.desensitizeAmount(fieldValue); case TIME - DesensitizeUtil.desensitizeTime(fieldValue); case ORDER_NO - DesensitizeUtil.desensitizeOrderNo(fieldValue); default - fieldValue; }; }作用根据脱敏类型调用工具类对应的方法解耦AOP和脱敏规则。辅助方法getCachedFields字段缓存private Field[] getCachedFields(Class? clazz) { if (fieldCache.containsKey(clazz)) return fieldCache.get(clazz); ListField fieldList new ArrayList(); Class? currentClazz clazz; // 递归获取父类字段处理继承场景 while (currentClazz ! null currentClazz ! Object.class) { fieldList.addAll(Arrays.asList(currentClazz.getDeclaredFields())); currentClazz currentClazz.getSuperclass(); } Field[] fields fieldList.toArray(new Field[0]); fieldCache.put(clazz, fields); return fields; }作用缓存类的所有字段包括父类避免重复反射提升性能。辅助方法isExcludeType排除框架类型private boolean isExcludeType(Class? clazz) { // 排除基础类型包装类 SetClass? basicTypes Set.of(Integer.class, Long.class, Double.class, Float.class, Boolean.class, Byte.class, Short.class, Character.class); if (basicTypes.contains(clazz)) return true; // 排除Date/数值类型 if (clazz Date.class || clazz BigDecimal.class || Number.class.isAssignableFrom(clazz)) return true; // 排除框架类避免反射处理Spring/MyBatis等对象 String className clazz.getName(); return className.startsWith(java.util.) !className.startsWith(java.util.List) !className.startsWith(java.util.Set) || className.startsWith(org.springblade.) !className.startsWith(org.springblade.business.entity) || className.startsWith(com.baomidou.mybatisplus.) || className.startsWith(jakarta.) || className.startsWith(org.springframework.); }作用避免对框架类如Spring的HashMap、MyBatis的Page进行无效反射防止报错。5. Date类型序列化器SensitiveDateSerializerpublic class SensitiveDateSerializer extends JsonSerializerDate { private static final SimpleDateFormat JSON_FORMAT new SimpleDateFormat(yyyy-MM-dd HH:mm:ss, Locale.CHINA); Override public void serialize(Date value, JsonGenerator gen, SerializerProvider serializers) throws IOException { if (value null) { gen.writeNull(); return; } // 获取当前序列化的对象和JSON字段名 Object currentObj gen.getCurrentValue(); String jsonFieldName gen.getOutputContext().getCurrentName(); // 查找字段的脱敏注解兼容驼峰/下划线、父类字段 SensitiveField sensitiveField findSensitiveField(currentObj.getClass(), jsonFieldName); // 无TIME注解正常序列化 if (sensitiveField null || sensitiveField.type() ! SensitiveType.TIME) { gen.writeString(JSON_FORMAT.format(value)); return; } // 有TIME注解全*脱敏 String dateStr JSON_FORMAT.format(value); gen.writeString(*.repeat(dateStr.length())); } // 递归查找字段注解兼容父类 private SensitiveField findSensitiveField(Class? clazz, String jsonFieldName) { if (clazz Object.class) return null; for (Field field : clazz.getDeclaredFields()) { String fieldName field.getName(); String jsonNameToCamel underlineToCamel(jsonFieldName); // 下划线转驼峰 if (fieldName.equals(jsonNameToCamel) || fieldName.equals(jsonFieldName)) { field.setAccessible(true); return field.getAnnotation(SensitiveField.class); } } return findSensitiveField(clazz.getSuperclass(), jsonFieldName); } // 下划线转驼峰适配JSON字段名 private String underlineToCamel(String str) { if (str null || str.isEmpty()) return str; StringBuilder sb new StringBuilder(); boolean nextUpper false; for (char c : str.toCharArray()) { if (c _) { nextUpper true; } else { sb.append(nextUpper ? Character.toUpperCase(c) : c); nextUpper false; } } return sb.toString(); } }核心解决的问题Date类型无法通过AOP直接脱敏字符串赋值给Date会报错JSON字段名可能是下划线如submit_time实体字段是驼峰submitTime需要格式转换兼容父类字段递归查找父类中的字段注解仅对标记TIME类型的Date字段脱敏其他Date字段正常序列化。6. 脱敏工具类DesensitizeUtil规则实现手机号脱敏desensitizePhonepublic static String desensitizePhone(String phone) { if (StringUtils.isBlank(phone) || phone.length() ! 11) return phone; // 正则替换保留前3后4中间4位* return phone.replaceAll((\\d{3})\\d{4}(\\d{4}), $1****$2); }规则11位手机号才脱敏避免非手机号字符串被错误处理。身份证脱敏desensitizeIdCardpublic static String desensitizeIdCard(String idCard) { if (StringUtils.isBlank(idCard)) return idCard; int length idCard.length(); if (length 18) { // 18位保留前6后4中间8位* return idCard.replaceAll((\\d{6})\\d{8}(\\d{4}), $1********$2); } else if (length 15) { // 15位保留前6后3中间6位* return idCard.replaceAll((\\d{6})\\d{6}(\\d{3}), $1******$2); } return idCard; }规则区分15/18位身份证适配不同长度的脱敏规则。姓名脱敏desensitizeNamepublic static String desensitizeName(String name) { if (StringUtils.isBlank(name)) return name; int length name.length(); if (length 1) return name; // 单字名不脱敏 // 处理复姓如欧阳、司马 String[] compoundSurnames {欧阳, 司马, 上官, 司徒, 夏侯, 诸葛, 闻人, 南宫, 万俟, 闻人, 赫连, 皇甫, 尉迟, 公羊}; String surname ; if (length 2) { String twoChar name.substring(0, 2); for (String cs : compoundSurnames) { if (cs.equals(twoChar)) { surname twoChar; break; } } } // 非复姓取单字 if (StringUtils.isBlank(surname)) surname name.substring(0, 1); // 无论剩余多少字只加1个*如张三丰→张*欧阳娜娜→欧阳* return surname *; }难点兼容复姓避免复姓被错误截断如“欧阳娜娜”脱敏为“欧阳*”而非“欧*”。地址脱敏desensitizeAddresspublic static String desensitizeAddress(String address) { if (StringUtils.isBlank(address)) return address; // 地址层级关键词优先级区/县 市 省 String[] levelKeywords { 区, 县, 旗, 自治县, 自治旗, 林区, 特区, 市, 自治州, 地区, 盟, 省, 自治区, 直辖市, 特别行政区 }; // 找到第一个层级关键词截断后续内容 for (String keyword : levelKeywords) { int keywordIndex address.indexOf(keyword); if (keywordIndex ! -1) { return StringUtils.trim(address.substring(0, keywordIndex keyword.length())); } } return address; }规则仅保留省/市/县区剔除街道、门牌号等敏感信息适配不同地区的地址格式如直辖市、自治区。其他工具方法密码脱敏固定返回6个*无论原密码长度订单号脱敏最后6位替换为*长度≤6则全*金额脱敏全*支持字符串和数值类型重载方法自定义脱敏保留指定前缀/后缀中间4个*避免前缀后缀长度超过字符串长度。三、重点难点总结面试高频1. 核心难点Date类型脱敏问题AOP中直接修改Date字段值会导致类型赋值冲突字符串→Date解决方案通过Jackson序列化器在JSON输出阶段脱敏不修改实体字段原值关键细节兼容JSON字段名驼峰/下划线、父类字段、JsonFormat格式。2. 性能优化点字段缓存反射获取字段是耗时操作缓存类的字段信息已脱敏对象缓存避免重复处理同一对象如集合中重复元素递归深度限制防止对象循环引用导致栈溢出ThreadLocal缓存保证多线程安全避免缓存污染。3. 兼容性设计适配通用返回对象支持RT、集合、数组、单个对象排除框架类型避免反射处理Spring/MyBatis等框架类防止报错父类字段兼容递归获取父类字段支持继承场景空值/异常处理所有脱敏方法都做了空值判断避免NPE。4. 扩展性设计枚举驱动新增脱敏类型只需在SensitiveType中添加补充工具类方法注解参数化自定义脱敏支持前缀/后缀长度配置工具类解耦脱敏规则与AOP/序列化器解耦便于修改规则。5. 面试高频问题Q1为什么Date类型不能通过AOP直接脱敏AAOP中通过反射修改字段值时若字段是Date类型脱敏后的字符串无法赋值给Date字段会抛出IllegalArgumentException因此选择在Jackson序列化阶段脱敏仅修改JSON输出内容不修改实体字段原值。Q2如何避免递归处理对象时的栈溢出A设置递归深度限制如MAX_RECURSION_DEPTH 10缓存已脱敏对象避免重复处理排除框架类型和基础类型减少递归次数。Q3如何提升脱敏框架的性能A缓存类的字段信息避免重复反射缓存已脱敏对象避免重复处理排除无需脱敏的类型基础类型、框架类型仅处理有SensitiveField注解的字段减少无效遍历。Q4如何扩展新的脱敏类型A在SensitiveType枚举中添加新类型如BANK_CARD在DesensitizeUtil中实现对应的脱敏方法如desensitizeBankCard在SensitiveAspect的getDesensitizedString方法中添加枚举分支在实体字段上添加SensitiveField(type SensitiveType.BANK_CARD)。Q5为什么要排除框架类型A框架类型如Spring的HashMap、MyBatis的Page无需脱敏且反射处理这些类可能会抛出权限异常因此通过isExcludeType方法排除只处理业务实体类。四、开箱即用使用指南1. 快速集成复制所有类到项目中枚举、注解、切面、序列化器、工具类确保依赖齐全Spring AOP、Jackson、Apache Commons Lang在Controller层接口方法上添加Sensitive注解在实体类敏感字段上添加SensitiveField注解指定脱敏类型Date类型字段添加JsonSerialize(using SensitiveDateSerializer.class)。2. 使用示例实体类public class User { SensitiveField(type SensitiveType.PHONE) private String phone; SensitiveField(type SensitiveType.ID_CARD) private String idCard; SensitiveField(type SensitiveType.NAME) private String realName; SensitiveField(type SensitiveType.TIME) JsonSerialize(using SensitiveDateSerializer.class) JsonFormat(pattern yyyy-MM-dd HH:mm:ss) private Date submitTime; SensitiveField(type SensitiveType.ADDRESS) private String address; }Controller层RestController RequestMapping(/user) public class UserController { PostMapping(/get) Sensitive // 标记该方法返回值需要脱敏 public RUser getUser() { User user new User(); user.setPhone(13812345678); user.setIdCard(110101199001011234); user.setRealName(张三丰); user.setSubmitTime(new Date()); user.setAddress(北京市朝阳区建国路88号); return R.ok(user); } }返回结果{ code: 200, success: true, data: { phone: 138****5678, idCard: 110101********1234, realName: 张*, submitTime: ******************, address: 北京市朝阳区 }, msg: 操作成功 }五、扩展建议支持更多类型如BigDecimal、LocalDateTime参考Date序列化器实现配置化脱敏规则将脱敏规则如手机号保留位数配置在yml文件中无需修改代码全局序列化器配置通过Jackson全局配置注册序列化器无需在每个Date字段添加JsonSerialize脱敏日志添加脱敏审计日志记录脱敏的字段、原值脱敏后、操作时间等自定义序列化器注解封装SensitiveDate注解简化Date字段的注解配置。完整版源码package org.springblade.business.enums; /** * 脱敏类型枚举 */ public enum SensitiveType { /** 手机号138****1234 */ PHONE, /** 身份证号110101********1234 */ ID_CARD, /** 姓名张*、张三丰→张* */ NAME, /** 密码全部打码 ****** */ PASSWORD, /** 自定义脱敏保留前2位、后2位 */ CUSTOM, /* 地址脱敏,仅保留省市区级如江西省赣州市于都县 */ ADDRESS, /* 金额脱敏全部打码 */ AMOUNT, /* 时间脱敏全部打码 */ TIME, /** 订单编号最后6位打码 */ ORDER_NO }package org.springblade.business.aspect.annotation; import java.lang.annotation.*; /** * 方法级注解标记该方法的返回值需要进行数据脱敏 */ Target(ElementType.METHOD) // 仅作用于方法 Retention(RetentionPolicy.RUNTIME) // 运行时保留AOP可反射获取 Documented public interface Sensitive { }package org.springblade.business.aspect.annotation; import org.springblade.business.enums.SensitiveType; import java.lang.annotation.*; /** * 字段级注解标记该字段需要脱敏并指定脱敏类型 */ Target(ElementType.FIELD) // 仅作用于字段 Retention(RetentionPolicy.RUNTIME) // 运行时保留AOP可反射获取 Documented public interface SensitiveField { /** * 脱敏类型必填 */ SensitiveType type(); /** * 自定义脱敏保留前缀长度默认2 */ int prefixLen() default 2; /** * 自定义脱敏保留后缀长度默认2 */ int suffixLen() default 2; }package org.springblade.business.aspect; import cn.hutool.json.JSONUtil; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springblade.business.aspect.annotation.SensitiveField; import org.springblade.business.enums.SensitiveType; import org.springblade.business.util.DesensitizeUtil; import org.springframework.stereotype.Component; import java.lang.reflect.Field; import java.math.BigDecimal; import java.util.*; import java.util.concurrent.ConcurrentHashMap; /** * 数据脱敏AOP切面 * 拦截带Sensitive注解的方法自动脱敏返回值中的敏感字段 */ Slf4j Aspect Component public class SensitiveAspect { // 递归深度限制避免无限递归 private static final int MAX_RECURSION_DEPTH 10; // 已脱敏对象缓存避免重复处理同一对象 private final ThreadLocalSetObject desensitizedCache ThreadLocal.withInitial(ConcurrentHashMap::newKeySet); // 字段缓存优化性能 private final MapClass?, Field[] fieldCache new ConcurrentHashMap(); /** * 切入点拦截所有带Sensitive注解的方法 */ Pointcut(annotation(org.springblade.business.aspect.annotation.Sensitive)) public void sensitivePointcut() { } /** * 返回通知方法执行成功后对返回值进行脱敏 */ AfterReturning(value sensitivePointcut(), returning result) public void desensitize(JoinPoint joinPoint, Object result) { log.info(脱敏切面:进入脱敏方法返回值类型{}, result.getClass().getName()); try { log.info(脱敏切面:开始脱敏原始数据{}, JSONUtil.toJsonStr(result)); // 核心脱敏逻辑初始化递归深度为0清空缓存 desensitizeObject(result, 0); log.info(脱敏切面:脱敏完成脱敏后数据{}, JSONUtil.toJsonStr(result)); } catch (Exception e) { log.error(脱敏切面:脱敏失败, e); } finally { // 清空线程缓存避免内存泄漏 desensitizedCache.get().clear(); desensitizedCache.remove(); } log.info(脱敏切面:脱敏完成正在退出脱敏方法); } /** * 递归处理对象脱敏增加递归深度限制,避免无限递归 * * param obj 待脱敏对象 * param depth 当前递归深度 */ private void desensitizeObject(Object obj, int depth) { // 终止条件1对象为空 if (obj null) { return; } // 终止条件2递归深度超过限制 if (depth MAX_RECURSION_DEPTH) { log.warn(【脱敏切面】递归深度超过{}终止脱敏{}, MAX_RECURSION_DEPTH, obj.getClass().getName()); return; } // 终止条件3对象已脱敏避免重复处理 if (desensitizedCache.get().contains(obj)) { return; } // 标记对象为已脱敏 desensitizedCache.get().add(obj); // 适配RT返回格式先获取data属性 if (obj.getClass().getSimpleName().equals(R)) { try { Field dataField obj.getClass().getDeclaredField(data); dataField.setAccessible(true); Object data dataField.get(obj); // 递归处理data深度1 desensitizeObject(data, depth 1); return; } catch (NoSuchFieldException | IllegalAccessException e) { log.warn(【脱敏切面】未找到R对象的data字段直接脱敏原对象); } } // 场景1集合List/Set→ 遍历元素脱敏 if (obj instanceof Collection? collection) { for (Object item : collection) { desensitizeObject(item, depth 1); } return; } // 场景2数组 → 遍历元素脱敏 if (obj.getClass().isArray()) { Object[] array (Object[]) obj; for (Object item : array) { desensitizeObject(item, depth 1); } return; } // 场景3单个业务对象 → 脱敏字段 desensitizeSingleObject(obj, depth 1); } /** * 处理单个对象的脱敏仅处理业务实体字段 */ private void desensitizeSingleObject(Object obj, int depth) { if (obj null) { return; } Class? clazz obj.getClass(); // 排除基础类型/String/DateDate交给序列化器处理 if (clazz.isPrimitive() || obj instanceof String || obj instanceof Date) { return; } try { Field[] fields getCachedFields(clazz); for (Field field : fields) { field.setAccessible(true); Object fieldValue field.get(obj); Class? fieldType field.getType(); // 核心跳过Date类型字段无论是否有注解 if (fieldType Date.class) { continue; } // 1. 无脱敏注解的字段按原有逻辑过滤 if (!field.isAnnotationPresent(SensitiveField.class)) { if (fieldValue ! null !isExcludeType(fieldType)) { desensitizeObject(fieldValue, depth 1); } continue; } // 2. 仅处理字符串类型的注解字段 if (fieldValue instanceof String strValue) { SensitiveField annotation field.getAnnotation(SensitiveField.class); SensitiveType type annotation.type(); int prefixLen annotation.prefixLen(); int suffixLen annotation.suffixLen(); String desensitizedStr getDesensitizedString(strValue, type, prefixLen, suffixLen); field.set(obj, desensitizedStr); } } } catch (IllegalAccessException e) { log.error(【脱敏切面】反射处理字段失败, e); } } /** * 仅处理字符串类型的脱敏移除类型转回逻辑避免赋值异常 */ private String getDesensitizedString(String fieldValue, SensitiveType type, int prefixLen, int suffixLen) { if (fieldValue.isBlank()) { return fieldValue; } return switch (type) { case PHONE - DesensitizeUtil.desensitizePhone(fieldValue); case ID_CARD - DesensitizeUtil.desensitizeIdCard(fieldValue); case NAME - DesensitizeUtil.desensitizeName(fieldValue); case PASSWORD - DesensitizeUtil.desensitizePassword(fieldValue); case CUSTOM - DesensitizeUtil.desensitizeCustom(fieldValue, prefixLen, suffixLen); case ADDRESS - DesensitizeUtil.desensitizeAddress(fieldValue); case AMOUNT - DesensitizeUtil.desensitizeAmount(fieldValue); case TIME - DesensitizeUtil.desensitizeTime(fieldValue); case ORDER_NO - DesensitizeUtil.desensitizeOrderNo(fieldValue); default - fieldValue; }; } /** * 缓存获取类的所有字段包括父类 */ private Field[] getCachedFields(Class? clazz) { if (fieldCache.containsKey(clazz)) { return fieldCache.get(clazz); } ListField fieldList new ArrayList(); Class? currentClazz clazz; // 遍历所有父类直到Object不限制TenantEntity while (currentClazz ! null currentClazz ! Object.class) { fieldList.addAll(Arrays.asList(currentClazz.getDeclaredFields())); currentClazz currentClazz.getSuperclass(); } Field[] fields fieldList.toArray(new Field[0]); fieldCache.put(clazz, fields); return fields; } /** * 判断是否为需要排除的类型核心避免递归处理框架/基础类型 */ private boolean isExcludeType(Class? clazz) { // 基础类型包装类 SetClass? basicTypes Set.of(Integer.class, Long.class, Double.class, Float.class, Boolean.class, Byte.class, Short.class, Character.class); if (basicTypes.contains(clazz)) { return true; } // 核心明确排除Date/数值类型 if (clazz Date.class || clazz BigDecimal.class || Number.class.isAssignableFrom(clazz)) { return true; } // 框架类型排除 String className clazz.getName(); return className.startsWith(java.util.) !className.startsWith(java.util.List) !className.startsWith(java.util.Set) || className.startsWith(org.springblade.) !className.startsWith(org.springblade.business.entity) || className.startsWith(com.baomidou.mybatisplus.) || className.startsWith(jakarta.) || className.startsWith(org.springframework.); } }package org.springblade.business.util; import org.apache.commons.lang3.StringUtils; /** * 数据脱敏工具类 * 脱敏规则 * 1. 姓名仅保留姓氏其余打*如刘德华→刘* * 2. 订单编号最后6位固定为****** * 3. 放款时间/放款金额全部替换为* * 4. 手机号/身份证/密码通用脱敏逻辑 * 5. 自定义脱敏保留指定前缀后缀中间打* */ public class DesensitizeUtil { /** * 手机号脱敏保留前3位、后4位中间4位打码 * 示例13812345678 → 138****5678 */ public static String desensitizePhone(String phone) { if (StringUtils.isBlank(phone) || phone.length() ! 11) { return phone; } return phone.replaceAll((\\d{3})\\d{4}(\\d{4}), $1****$2); } /** * 身份证号脱敏保留前6位、后4位中间打码 * 示例110101199001011234 → 110101********1234 */ public static String desensitizeIdCard(String idCard) { if (StringUtils.isBlank(idCard)) { return idCard; } int length idCard.length(); if (length 18) { return idCard.replaceAll((\\d{6})\\d{8}(\\d{4}), $1********$2); } else if (length 15) { return idCard.replaceAll((\\d{6})\\d{6}(\\d{3}), $1******$2); } return idCard; } /** * 姓名脱敏仅保留姓氏单姓/复姓其余字符统一只打1个* * 示例 * 单字名 → 张不变 * 单姓多字 → 张三→张*、刘德华→刘* * 复姓 → 欧阳娜娜→欧阳*、司马相如→司马* */ public static String desensitizeName(String name) { if (StringUtils.isBlank(name)) { return name; } int length name.length(); // 1. 单字名直接返回 if (length 1) { return name; } // 2. 定义常见复姓可根据业务扩展 String[] compoundSurnames {欧阳, 司马, 上官, 司徒, 夏侯, 诸葛, 闻人, 南宫, 万俟, 闻人, 赫连, 皇甫, 尉迟, 公羊}; String surname ; // 3. 判断是否为复姓优先匹配复姓避免单姓误判 if (length 2) { String twoChar name.substring(0, 2); for (String cs : compoundSurnames) { if (cs.equals(twoChar)) { surname twoChar; break; } } } // 4. 非复姓则取单字为姓 if (StringUtils.isBlank(surname)) { surname name.substring(0, 1); } // 5. 无论剩余字符多少只加1个* return surname *; } /** * 密码脱敏全部打码 * 示例123456 → ****** */ public static String desensitizePassword(String password) { if (StringUtils.isBlank(password)) { return password; } return ******; } /** * 订单编号脱敏最后6位固定替换为****** * 示例ORDER123456789 → ORDER123******、123456 → ****** */ public static String desensitizeOrderNo(String orderNo) { if (StringUtils.isBlank(orderNo)) { return orderNo; } int length orderNo.length(); // 长度≤6时全部替换为****** if (length 6) { return ******; } // 长度6时保留前面部分最后6位替换为****** String prefix orderNo.substring(0, length - 6); return prefix ******; } /** * 时间脱敏全部替换为*长度与原字符串一致 * 示例2025-12-09 10:00:00 → ****************** */ public static String desensitizeTime(String time) { if (time null || time.isBlank()) { return time; } return *.repeat(time.length()); } /** * 放款金额脱敏全部替换为*长度与原字符串一致 * 示例10000.00 → ******* */ public static String desensitizeAmount(String amount) { if (StringUtils.isBlank(amount)) { return amount; } return *.repeat(amount.length()); } /** * 自定义脱敏保留指定前缀和后缀长度中间打码 */ public static String desensitizeCustom(String str, int prefixLen, int suffixLen) { if (StringUtils.isBlank(str) || prefixLen suffixLen str.length()) { return str; } String prefix str.substring(0, prefixLen); String suffix str.substring(str.length() - suffixLen); return prefix **** suffix; } // 重载适配数值类型的放款金额如BigDecimal/Integer/Double public static String desensitizeAmount(Number amount) { if (amount null) { return null; } String amountStr amount.toString(); return *.repeat(amountStr.length()); } /** * 详细地址脱敏仅保留省/市/县区层级剔除街道、道路、门牌号等详细信息 * 适配场景 * 1. 普通地址湖北省神农架林区347国道北侧 → 湖北省神农架林区 * 2. 直辖市北京市朝阳区建国路88号 → 北京市朝阳区 * 3. 无省信息杭州市西湖区文三路 → 杭州市西湖区 * 4. 仅省/市广东省深圳市 → 广东省深圳市 */ public static String desensitizeAddress(String address) { // 空值/空白串直接返回 if (StringUtils.isBlank(address)) { return address; } // 定义地址层级关键词优先级区/县 市 省匹配到则截断后续内容 String[] levelKeywords { 区, 县, 旗, 自治县, 自治旗, 林区, 特区, // 县级关键词核心截断标识 市, 自治州, 地区, 盟, // 市级关键词无县级时截断 省, 自治区, 直辖市, 特别行政区 // 省级关键词仅保留省 }; // 遍历关键词找到第一个匹配的层级末尾截断后续内容 for (String keyword : levelKeywords) { int keywordIndex address.indexOf(keyword); if (keywordIndex ! -1) { // 截取到关键词末尾如神农架林区中区在最后截取到区 String result address.substring(0, keywordIndex keyword.length()); // 去除截断后末尾的多余空格/特殊字符 return StringUtils.trim(result); } } // 无匹配层级关键词时返回原地址避免误截断 return address; } }package org.springblade.business.config; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import org.springblade.business.aspect.annotation.SensitiveField; import org.springblade.business.enums.SensitiveType; import java.io.IOException; import java.lang.reflect.Field; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; /** * 需要在date格式的字段上填下如下注解日期才能实现脱敏指定脱敏序列化器 * //JsonSerialize(using SensitiveDateSerializer.class) */ public class SensitiveDateSerializer extends JsonSerializerDate { // 适配JsonFormat的日期格式 private static final SimpleDateFormat JSON_FORMAT new SimpleDateFormat(yyyy-MM-dd HH:mm:ss, Locale.CHINA); Override public void serialize(Date value, JsonGenerator gen, SerializerProvider serializers) throws IOException { if (value null) { gen.writeNull(); return; } // 1. 获取当前序列化的实体对象和字段名JSON字段名 Object currentObj gen.getCurrentValue(); String jsonFieldName gen.getOutputContext().getCurrentName(); SensitiveField sensitiveField null; // 2. 遍历实体类父类字段匹配字段兼容驼峰/下划线 sensitiveField findSensitiveField(currentObj.getClass(), jsonFieldName); // 3. 无TIME脱敏注解 → 正常序列化按JsonFormat格式 if (sensitiveField null || sensitiveField.type() ! SensitiveType.TIME) { gen.writeString(JSON_FORMAT.format(value)); return; } // 4. 有TIME注解 → 按yyyy-MM-dd HH:mm:ss格式脱敏 String dateStr JSON_FORMAT.format(value); gen.writeString(*.repeat(dateStr.length())); } /** * 递归查找字段的SensitiveField注解兼容驼峰/下划线、父类字段 */ private SensitiveField findSensitiveField(Class? clazz, String jsonFieldName) { // 终止条件已到Object类 if (clazz Object.class) { return null; } // 遍历当前类所有字段 for (Field field : clazz.getDeclaredFields()) { // 匹配规则实体字段名驼峰 JSON字段名下划线转驼峰 String fieldName field.getName(); String jsonNameToCamel underlineToCamel(jsonFieldName); if (fieldName.equals(jsonNameToCamel) || fieldName.equals(jsonFieldName)) { field.setAccessible(true); return field.getAnnotation(SensitiveField.class); } } // 递归查找父类 return findSensitiveField(clazz.getSuperclass(), jsonFieldName); } /** * 下划线转驼峰适配JSON字段名 */ private String underlineToCamel(String str) { if (str null || str.isEmpty()) { return str; } StringBuilder sb new StringBuilder(); boolean nextUpper false; for (char c : str.toCharArray()) { if (c _) { nextUpper true; } else { sb.append(nextUpper ? Character.toUpperCase(c) : c); nextUpper false; } } return sb.toString(); } }注释要使用这个框架的话只需要在接口处添加如下注解然后需要在你的VO中添加对应注解如果是date数据类型还需要添加日期序列化注解对于日期格式还需添加如下注解测试结果如下所示