在白嫖的时候,希望你会内疚,一键三连吧,源码在最后,自取
在白嫖的时候,希望你会内疚,一键三连吧,源码在最后,自取
在白嫖的时候,希望你会内疚,一键三连吧,源码在最后,自取
一、整体架构与设计思路
核心目标
实现「无侵入、可扩展、多类型」的数据脱敏,支持字符串/日期/数值等类型,适配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 ThreadLocal<Set<Object>> desensitizedCache = ThreadLocal.withInitial(ConcurrentHashMap::newKeySet); // 字段缓存(缓存类的字段信息,避免重复反射,提升性能) private final Map<Class<?>, 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(); } }作用:方法执行完成后,对返回值进行脱敏处理,最终清空缓存。
核心递归方法:desensitizeObject
private 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); // 标记已脱敏 // 适配R<T>返回格式(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); }核心逻辑:
- 终止条件:空对象、递归超限、已脱敏对象,避免无效处理;
- 适配通用返回对象
R<T>:仅处理data属性中的业务数据; - 兼容集合/数组:遍历元素逐个脱敏;
- 单个对象:交给
desensitizeSingleObject处理字段级脱敏。
字段级脱敏:desensitizeSingleObject
private void desensitizeSingleObject(Object obj, int depth) { Class<?> clazz = obj.getClass(); // 排除基础类型/String/Date(Date交给序列化器处理) 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)替换原始值。
工具类调用:getDesensitizedString
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; }; }作用:根据脱敏类型,调用工具类对应的方法,解耦AOP和脱敏规则。
辅助方法:getCachedFields(字段缓存)
private Field[] getCachedFields(Class<?> clazz) { if (fieldCache.containsKey(clazz)) return fieldCache.get(clazz); List<Field> 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) { // 排除基础类型包装类 Set<Class<?>> 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类型序列化器:SensitiveDateSerializer
public class SensitiveDateSerializer extends JsonSerializer<Date> { 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(规则实现)
手机号脱敏:desensitizePhone
public 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位手机号才脱敏,避免非手机号字符串被错误处理。
身份证脱敏:desensitizeIdCard
public 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位身份证,适配不同长度的脱敏规则。
姓名脱敏:desensitizeName
public 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 + "*"; }难点:兼容复姓,避免复姓被错误截断(如“欧阳娜娜”脱敏为“欧阳*”而非“欧*”)。
地址脱敏:desensitizeAddress
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) { 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. 兼容性设计
- 适配通用返回对象:支持
R<T>、集合、数组、单个对象; - 排除框架类型:避免反射处理Spring/MyBatis等框架类,防止报错;
- 父类字段兼容:递归获取父类字段,支持继承场景;
- 空值/异常处理:所有脱敏方法都做了空值判断,避免NPE。
4. 扩展性设计
- 枚举驱动:新增脱敏类型只需在
SensitiveType中添加,补充工具类方法; - 注解参数化:自定义脱敏支持前缀/后缀长度配置;
- 工具类解耦:脱敏规则与AOP/序列化器解耦,便于修改规则。
5. 面试高频问题
Q1:为什么Date类型不能通过AOP直接脱敏?
A:AOP中通过反射修改字段值时,若字段是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 R<User> 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 ThreadLocal<Set<Object>> desensitizedCache = ThreadLocal.withInitial(ConcurrentHashMap::newKeySet); // 字段缓存(优化性能) private final Map<Class<?>, 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); // 适配R<T>返回格式:先获取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/Date(Date交给序列化器处理) 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); } List<Field> 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) { // 基础类型包装类 Set<Class<?>> 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 JsonSerializer<Date> { // 适配@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数据类型,还需要添加日期序列化注解:
对于日期格式,还需添加如下注解:
测试结果,如下所示: