在白嫖之前,希望你会内疚,一键三连OK?完整版源码在最后,CV微改即可;
在白嫖之前,希望你会内疚,一键三连OK?完整版源码在最后,CV微改即可;
在白嫖之前,希望你会内疚,一键三连OK?完整版源码在最后,CV微改即可;

一、整体架构与设计思路
核心目标
实现「无侵入、可扩展、多类型」的数据脱敏,支持字符串/日期/数值等类型,适配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数据类型,还需要添加日期序列化注解:

对于日期格式,还需添加如下注解:

测试结果,如下所示:
