大家好,我是小悟。
一、数据脱敏:数据界的"犹抱琵琶半遮面"
想象一下这样的场景:你的身份证号、手机号、银行卡号这些"隐私部位"的数据,在系统中裸奔------这简直比在公共场所穿皇帝的新衣还尴尬!数据脱敏就是给这些敏感数据穿上得体的"小内裤",让它们在需要展示的时候既能完成工作,又不至于春光乍泄。
数据脱敏的几种常见姿势:
- 静态脱敏:像给照片打马赛克,一劳永逸
- 动态脱敏:像智能变色玻璃,看人下菜碟
- 前端脱敏:只在展示时害羞一下
- 后端脱敏:从出生就带着面具
二、SpringBoot脱敏方案实战
方案1:注解+序列化方案(给字段贴上"此处打码"标签)
步骤1:先来个脱敏注解,像给敏感部位贴标签
swift
import java.lang.annotation.*;
/**
* 脱敏注解:给敏感字段贴上"此处需要打码"的标签
* 就像在数据身上贴了个"儿童不宜"的警示条
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Sensitive {
/**
* 脱敏类型:决定怎么打码
*/
SensitiveType type();
}
/**
* 脱敏类型枚举:各种打码方式任君选择
*/
public enum SensitiveType {
/** 中文名:张*三 */
CHINESE_NAME,
/** 身份证号:110**********1234 */
ID_CARD,
/** 手机号:138****1234 */
PHONE,
/** 邮箱:t***@163.com */
EMAIL,
/** 银行卡号:6217 **** **** 1234 */
BANK_CARD,
/** 地址:北京市海淀区**** */
ADDRESS
}
步骤2:实现脱敏序列化器,专业的"打码师"
kotlin
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.io.IOException;
/**
* 脱敏序列化器:专业的"马赛克师傅"
* 负责给敏感数据穿上得体的衣服
*/
public class SensitiveSerializer extends JsonSerializer<String> {
private final SensitiveType type;
public SensitiveSerializer(SensitiveType type) {
this.type = type;
}
@Override
public void serialize(String value, JsonGenerator gen,
SerializerProvider serializers) throws IOException {
if (value == null) {
gen.writeNull();
return;
}
// 根据脱敏类型选择不同的"打码姿势"
gen.writeString(maskData(value, type));
}
/**
* 核心脱敏逻辑:十八般武艺轮番上阵
*/
private String maskData(String data, SensitiveType type) {
if (data == null || data.isEmpty()) {
return data;
}
return switch (type) {
case CHINESE_NAME -> maskChineseName(data);
case ID_CARD -> maskIdCard(data);
case PHONE -> maskPhone(data);
case EMAIL -> maskEmail(data);
case BANK_CARD -> maskBankCard(data);
case ADDRESS -> maskAddress(data);
default -> data; // 默认不脱敏,裸奔!
};
}
private String maskChineseName(String name) {
if (name.length() <= 1) return name;
if (name.length() == 2) return name.charAt(0) + "*";
return name.charAt(0) + "*" + name.charAt(name.length() - 1);
}
private String maskIdCard(String idCard) {
if (idCard.length() <= 8) return idCard;
return idCard.substring(0, 3) +
"*".repeat(Math.max(0, idCard.length() - 7)) +
idCard.substring(idCard.length() - 4);
}
private String maskPhone(String phone) {
if (phone.length() != 11) return phone;
return phone.substring(0, 3) + "****" + phone.substring(7);
}
private String maskEmail(String email) {
int atIndex = email.indexOf("@");
if (atIndex <= 1) return email;
return email.charAt(0) + "***" + email.substring(atIndex);
}
private String maskBankCard(String card) {
if (card.length() <= 8) return card;
return card.substring(0, 4) + " **** **** " +
card.substring(card.length() - 4);
}
private String maskAddress(String address) {
if (address.length() <= 4) return address;
return address.substring(0, address.length() - 4) + "****";
}
}
/**
* 注解序列化器:把注解和序列化器牵线搭桥
*/
public class SensitiveAnnotationIntrospector extends JacksonAnnotationIntrospector {
@Override
public Object findSerializer(Annotated am) {
Sensitive sensitive = am.getAnnotation(Sensitive.class);
if (sensitive != null) {
return new SensitiveSerializer(sensitive.type());
}
return super.findSerializer(am);
}
}
步骤3:配置Jackson,告诉它:"看这里,要打码!"
java
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.setAnnotationIntrospector(new SensitiveAnnotationIntrospector());
return mapper;
}
}
步骤4:在实体类上使用,贴上标签就自动打码
typescript
/**
* 用户实体:敏感字段都穿上了"马赛克小内裤"
*/
@Data
public class UserDTO {
private Long id;
@Sensitive(type = SensitiveType.CHINESE_NAME)
private String username;
@Sensitive(type = SensitiveType.PHONE)
private String phone;
@Sensitive(type = SensitiveType.EMAIL)
private String email;
@Sensitive(type = SensitiveType.ID_CARD)
private String idCard;
@Sensitive(type = SensitiveType.BANK_CARD)
private String bankCard;
@Sensitive(type = SensitiveType.ADDRESS)
private String address;
// 这个字段没注解,继续裸奔
private String hobby;
}
步骤5:控制器测试一下效果
less
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/{id}")
public UserDTO getUser(@PathVariable Long id) {
// 模拟从数据库查出的完整数据
UserDTO user = new UserDTO();
user.setId(id);
user.setUsername("张全蛋");
user.setPhone("13800138000");
user.setEmail("zhangquandan@example.com");
user.setIdCard("110101199001011234");
user.setBankCard("621700001234567890");
user.setAddress("北京市海淀区中关村大街1号");
user.setHobby("唱跳RAP篮球");
// 返回时自动脱敏,就像自动加了马赛克
return user;
}
}
测试结果:
perl
{
"id": 1,
"username": "张*蛋",
"phone": "138****8000",
"email": "z***@example.com",
"idCard": "110**********1234",
"bankCard": "6217 **** **** 7890",
"address": "北京市海淀区中关村大街****",
"hobby": "唱跳RAP篮球"
}
方案2:AOP切面方案(数据出门前的安检员)
步骤1:定义脱敏策略接口
typescript
/**
* 脱敏策略:定义各种脱敏算法
* 就像不同的美颜滤镜
*/
public interface SensitiveStrategy {
String mask(String data);
}
/**
* 策略工厂:根据类型选择合适的滤镜
*/
@Component
public class SensitiveStrategyFactory {
private final Map<SensitiveType, SensitiveStrategy> strategies = new HashMap<>();
public SensitiveStrategyFactory() {
// 注册各种美颜滤镜
strategies.put(SensitiveType.CHINESE_NAME, new ChineseNameStrategy());
strategies.put(SensitiveType.PHONE, new PhoneStrategy());
strategies.put(SensitiveType.ID_CARD, new IdCardStrategy());
// ... 其他策略
}
public SensitiveStrategy getStrategy(SensitiveType type) {
return strategies.getOrDefault(type, data -> data);
}
// 具体策略实现
private static class ChineseNameStrategy implements SensitiveStrategy {
@Override
public String mask(String data) {
if (data == null || data.length() <= 1) return data;
if (data.length() == 2) return data.charAt(0) + "*";
return data.charAt(0) + "*" + data.charAt(data.length() - 1);
}
}
private static class PhoneStrategy implements SensitiveStrategy {
@Override
public String mask(String data) {
if (data == null || data.length() != 11) return data;
return data.substring(0, 3) + "****" + data.substring(7);
}
}
// ... 其他策略实现
}
步骤2:AOP切面实现
scss
@Aspect
@Component
@Slf4j
public class SensitiveAspect {
@Autowired
private SensitiveStrategyFactory strategyFactory;
/**
* 拦截所有Controller方法返回
* 就像在数据出门前设了个安检门
*/
@Around("@annotation(org.springframework.web.bind.annotation.GetMapping) || " +
"@annotation(org.springframework.web.bind.annotation.PostMapping) || " +
"@annotation(org.springframework.web.bind.annotation.RequestMapping)")
public Object aroundController(ProceedingJoinPoint joinPoint) throws Throwable {
// 放行方法执行
Object result = joinPoint.proceed();
// 给返回结果穿上衣服
return processSensitiveData(result);
}
/**
* 递归处理脱敏:连数据对象的子孙后代都不放过
*/
private Object processSensitiveData(Object obj) {
if (obj == null) return null;
// 如果是集合,给每个元素都穿上衣服
if (obj instanceof Collection) {
return processCollection((Collection<?>) obj);
}
// 如果是数组,也不放过
if (obj.getClass().isArray()) {
return processArray((Object[]) obj);
}
// 如果是Map,处理每个值
if (obj instanceof Map) {
return processMap((Map<?, ?>) obj);
}
// 如果是普通对象,深度扫描敏感字段
if (isCustomClass(obj.getClass())) {
return processObject(obj);
}
// 基本类型,直接返回
return obj;
}
private Object processObject(Object obj) {
Class<?> clazz = obj.getClass();
Object newObj;
try {
newObj = clazz.newInstance();
} catch (Exception e) {
log.warn("创建对象实例失败: {}", clazz.getName());
return obj;
}
// 反射获取所有字段
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
try {
Object value = field.get(obj);
// 如果有脱敏注解,穿上马赛克
Sensitive sensitive = field.getAnnotation(Sensitive.class);
if (sensitive != null && value instanceof String) {
SensitiveStrategy strategy = strategyFactory.getStrategy(sensitive.type());
value = strategy.mask((String) value);
} else if (value != null) {
// 递归处理嵌套对象
value = processSensitiveData(value);
}
field.set(newObj, value);
} catch (Exception e) {
log.warn("处理字段 {} 失败", field.getName(), e);
}
}
return newObj;
}
}
方案3:MyBatis拦截器方案(数据库查询时的美颜相机)
typescript
/**
* MyBatis拦截器:在数据从数据库出来时实时美颜
*/
@Intercepts({
@Signature(type = ResultSetHandler.class,
method = "handleResultSets",
args = {Statement.class})
})
@Component
@Slf4j
public class SensitiveInterceptor implements Interceptor {
@Autowired
private SensitiveStrategyFactory strategyFactory;
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 先执行原方法获取结果
Object result = invocation.proceed();
if (result == null) {
return null;
}
// 处理结果集
if (result instanceof List) {
for (Object obj : (List<?>) result) {
processObject(obj);
}
} else {
processObject(result);
}
return result;
}
private void processObject(Object obj) {
if (obj == null) return;
Class<?> clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
Sensitive sensitive = field.getAnnotation(Sensitive.class);
if (sensitive != null) {
field.setAccessible(true);
try {
Object value = field.get(obj);
if (value instanceof String) {
SensitiveStrategy strategy = strategyFactory.getStrategy(sensitive.type());
String maskedValue = strategy.mask((String) value);
field.set(obj, maskedValue);
}
} catch (Exception e) {
log.error("脱敏处理失败", e);
}
}
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 可以配置一些属性
}
}
方案4:自定义消息转换器方案(HTTP出口处的安检机)
scala
/**
* 自定义HTTP消息转换器:在数据离开系统前最后一道安检
*/
@Component
public class SensitiveHttpMessageConverter extends MappingJackson2HttpMessageConverter {
@Autowired
private SensitiveStrategyFactory strategyFactory;
@Override
protected void writeInternal(Object object, Type type,
HttpOutputMessage outputMessage) throws IOException {
// 先脱敏再序列化
Object processedObject = processSensitiveData(object);
super.writeInternal(processedObject, type, outputMessage);
}
// 脱敏处理方法(同上,省略重复代码)
private Object processSensitiveData(Object obj) {
// 实现同AOP方案中的processSensitiveData方法
// ...
}
}
方案5:数据库层脱敏方案(给数据库戴上口罩)
typescript
/**
* Hibernate事件监听器:数据入库时自动加密,出库时自动解密
*/
@Component
public class SensitiveEventListener implements
PostLoadEventListener, PreInsertEventListener, PreUpdateEventListener {
@Autowired
private EncryptionService encryptionService;
@Override
public void onPostLoad(PostLoadEvent event) {
Object entity = event.getEntity();
// 加载后解密
decryptEntity(entity);
}
@Override
public boolean onPreInsert(PreInsertEvent event) {
// 插入前加密
encryptEntity(event.getEntity());
return false;
}
@Override
public boolean onPreUpdate(PreUpdateEvent event) {
// 更新前加密
encryptEntity(event.getEntity());
return false;
}
private void encryptEntity(Object entity) {
if (entity == null) return;
Field[] fields = entity.getClass().getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(SensitiveEncrypt.class)) {
field.setAccessible(true);
try {
Object value = field.get(entity);
if (value instanceof String) {
String encrypted = encryptionService.encrypt((String) value);
field.set(entity, encrypted);
}
} catch (Exception e) {
log.error("加密字段失败", e);
}
}
}
}
private void decryptEntity(Object entity) {
// 类似encryptEntity,调用encryptionService.decrypt
}
}
三、脱敏方案选择指南:对症下药
1. 注解+序列化方案
适用场景 :REST API返回数据脱敏 优点 :简单优雅,与业务解耦 缺点:只对JSON序列化有效
2. AOP切面方案
适用场景 :需要对Controller层统一处理 优点 :集中管理,支持复杂逻辑 缺点:性能开销,可能误伤
3. MyBatis拦截器方案
适用场景 :数据库查询结果脱敏 优点 :从源头控制,一劳永逸 缺点:影响所有查询,不够灵活
4. 自定义消息转换器方案
适用场景 :全局HTTP响应处理 优点 :最彻底的出口控制 缺点:可能与其他组件冲突
5. 数据库层方案
适用场景 :存储加密,展示脱敏 优点 :最安全,防止数据泄露 缺点:影响查询性能,实现复杂
四、最佳实践建议
1. 分层防御:不要把所有鸡蛋放在一个篮子里
markdown
数据安全防护体系:
- 存储层:加密存储(最后的底线)
- 业务层:逻辑脱敏(灵活控制)
- 展示层:展示脱敏(用户体验)
2. 配置化脱敏:像调美颜强度一样可配置
less
@Component
@ConfigurationProperties(prefix = "sensitive")
@Data
public class SensitiveProperties {
/**
* 是否开启脱敏
*/
private boolean enabled = true;
/**
* 脱敏规则配置
*/
private Map<SensitiveType, Rule> rules = new HashMap<>();
@Data
public static class Rule {
/**
* 保留前几位
*/
private Integer keepPrefix = 3;
/**
* 保留后几位
*/
private Integer keepSuffix = 4;
/**
* 替换字符
*/
private Character maskChar = '*';
}
}
3. 性能优化:脱敏也要注意效率
typescript
@Component
public class SensitiveCache {
private final Cache<String, String> cache =
Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
public String maskWithCache(String data, SensitiveType type,
SensitiveStrategy strategy) {
String key = type.name() + ":" + data;
return cache.get(key, k -> strategy.mask(data));
}
}
4. 监控与日志:知道谁在什么时候脱敏
less
@Aspect
@Component
@Slf4j
public class SensitiveMonitorAspect {
@Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
public Object monitorSensitive(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long cost = System.currentTimeMillis() - start;
// 记录脱敏统计
log.info("脱敏处理完成,方法:{},耗时:{}ms",
joinPoint.getSignature(), cost);
return result;
}
}
五、总结:数据脱敏的智慧
数据脱敏就像给敏感数据穿上得体的衣服------既不能裸奔(安全风险),也不能裹成木乃伊(影响使用)。通过SpringBoot的各种方案,我们可以:
- 因地制宜:根据不同的场景选择合适的脱敏方案
- 层层设防:构建多层次的数据安全防护体系
- 灵活配置:像调节美颜相机一样轻松调整脱敏策略
- 性能平衡:在安全和性能之间找到最佳平衡点
没有一种方案是万能的。就像穿衣服要分场合(泳池穿泳衣,会议室穿正装),数据脱敏也要根据具体场景选择最合适的方案。
最终目标:让敏感数据既能保守秘密,又能履行职责。毕竟,数据的价值在于使用,而不是锁在保险柜里吃灰。脱敏就是让数据在"安全"和"可用"之间优雅地走钢丝!
typescript
// 最后送大家一个万能脱敏方法
public String universalMask(String data) {
return "****"; // 简单粗暴,但最安全!(开玩笑的,别真用)
}
过多的脱敏会影响业务,过少的脱敏又存在风险。找到那个刚刚好的平衡点,才是数据脱敏的最高境界!

谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。
您的一键三连,是我更新的最大动力,谢谢
山水有相逢,来日皆可期,谢谢阅读,我们再会
我手中的金箍棒,上能通天,下能探海