SpringBoot动态脱敏实战:从注解到AOP的优雅打码术
前言
最近在做项目时遇到一个头疼的问题:业务方要求对用户敏感信息进行脱敏处理,比如手机号、身份证、邮箱等。如果手动处理,不仅代码冗余,还容易遗漏。有没有一种优雅的解决方案呢?
答案是肯定的!今天就来分享一套基于SpringBoot + 注解 + AOP的动态脱敏方案,让你的敏感数据自动"戴上面具"。
为什么要做数据脱敏?
在互联网时代,数据安全和隐私保护越来越重要。无论是响应《数据安全法》还是满足业务需求,数据脱敏都成了开发者的必修课。
想象一下,如果用户的真实手机号、身份证号直接暴露在日志或前端页面中,那后果不堪设想。数据脱敏就像给敏感信息穿上"隐身衣",既能保证业务正常流转,又能保护用户隐私。
核心设计思路
我们的目标是实现一个"无侵入"的数据脱敏方案,即:
- 业务代码无需感知脱敏逻辑
- 通过注解标记需要脱敏的字段
- AOP自动拦截并处理返回数据
这种设计的核心思想是"关注点分离",让业务逻辑专注于业务本身,脱敏逻辑交给框架自动处理。
实现方案详解
1. 定义脱敏类型枚举
首先,我们需要定义常见的脱敏类型:
public enum SensitiveTypeEnum {
/**
* 中文姓名脱敏(保留姓氏,其余用*代替)
* 示例:张三 -> 张*
*/
CHINESE_NAME(1, "中文姓名"),
/**
* 身份证号码脱敏(保留前3位和后3位)
* 示例:110101199003071234 -> 110****1234
*/
ID_CARD(2, "身份证号码"),
/**
* 手机号码脱敏(保留前3位和后4位)
* 示例:13812345678 -> 138****5678
*/
PHONE(3, "手机号码"),
// ... 其他类型
}
2. 创建脱敏注解
定义一个自定义注解,用于标记需要脱敏的字段:
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveData {
// 脱敏类型
SensitiveTypeEnum type() default SensitiveTypeEnum.CUSTOM;
// 前缀保留长度
int prefixKeep() default 0;
// 后缀保留长度
int suffixKeep() default 0;
// 替换字符
String maskChar() default "*";
// 是否启用脱敏
boolean enabled() default true;
}
3. 实现脱敏处理器
创建脱敏工具类,实现各种脱敏算法:
public class SensitiveDataHandler {
public static String mask(String value, SensitiveData sensitiveData) {
if (value == null || value.isEmpty() || !sensitiveData.enabled()) {
return value;
}
switch (sensitiveData.type()) {
case CHINESE_NAME:
return maskChineseName(value);
case PHONE:
return maskPhone(value);
// ... 其他类型处理
default:
return value;
}
}
public static String maskPhone(String phone) {
if (phone == null || phone.length() != 11) {
return phone;
}
return phone.substring(0, 3) + "****" + phone.substring(7);
}
// 其他脱敏方法...
}
4. AOP切面实现
这是整个方案的核心,通过AOP拦截Controller的返回值:
@Aspect
@Order(1)
@Component
public class SensitiveDataAspect {
@Around("execution(* *(..)) && (@annotation(org.springframework.web.bind.annotation.GetMapping) || " +
"@annotation(org.springframework.web.bind.annotation.PostMapping) || " +
"@annotation(org.springframework.web.bind.annotation.PutMapping) || " +
"@annotation(org.springframework.web.bind.annotation.DeleteMapping) || " +
"@annotation(org.springframework.web.bind.annotation.RequestMapping))")
public Object around(ProceedingJoinPoint point) throws Throwable {
// 执行原方法
Object result = point.proceed();
// 对返回结果进行脱敏处理
if (result != null) {
result = processSensitiveData(result);
}
return result;
}
private Object processSensitiveData(Object obj) {
if (obj == null) {
return obj;
}
if (obj instanceof Collection) {
// 处理集合类型
Collection<?> collection = (Collection<?>) obj;
for (Object item : collection) {
processSensitiveData(item);
}
} else if (obj instanceof Map) {
// 处理Map类型
Map<?, ?> map = (Map<?, ?>) obj;
for (Object value : map.values()) {
processSensitiveData(value);
}
} else {
// 处理普通对象
processObjectSensitiveData(obj);
}
return obj;
}
private void processObjectSensitiveData(Object obj) {
if (obj == null) {
return;
}
Class<?> clazz = obj.getClass();
// 处理当前类及其父类的所有字段
while (clazz != null && !clazz.equals(Object.class)) {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
try {
field.setAccessible(true);
Object value = field.get(obj);
// 检查字段是否有@SensitiveData注解
if (field.isAnnotationPresent(SensitiveData.class)) {
SensitiveData sensitiveData = field.getAnnotation(SensitiveData.class);
if (value instanceof String) {
String originalValue = (String) value;
String maskedValue = SensitiveDataHandler.mask(originalValue, sensitiveData);
field.set(obj, maskedValue);
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
clazz = clazz.getSuperclass();
}
}
}
实际应用示例
定义一个用户信息实体类:
public class UserInfo {
@SensitiveData(type = SensitiveTypeEnum.CHINESE_NAME)
private String name;
@SensitiveData(type = SensitiveTypeEnum.PHONE)
private String phone;
@SensitiveData(type = SensitiveTypeEnum.EMAIL)
private String email;
// ... getter/setter
}
在Controller中正常使用:
@RestController
@RequestMapping("/api/user")
public class UserController {
@GetMapping("/info")
public UserInfo getUserInfo() {
return new UserInfo(
"张三丰", // 脱敏后:张**
"13812345678", // 脱敏后:138****5678
"zhang@example.com" // 脱敏后:z****g@...
);
}
}
方案优势
- 无侵入性:业务代码无需感知脱敏逻辑
- 灵活配置:支持多种脱敏类型和自定义规则
- 性能友好:仅在数据返回时进行脱敏,不影响业务处理
- 易于扩展:新增脱敏类型只需扩展枚举和处理方法
- 类型安全:编译期即可检查注解使用是否正确
注意事项
- 性能考虑:对于高频接口,要考虑脱敏操作的性能开销
- 缓存策略:脱敏后的数据通常不应被缓存
- 日志处理:确保敏感信息不会在日志中明文输出
- 异常处理:脱敏过程中的异常不应影响正常业务流程
总结
通过这套基于注解和AOP的动态脱敏方案,我们可以实现:
- 业务代码的简洁性:无需在业务逻辑中处理脱敏
- 配置的灵活性:通过注解参数灵活控制脱敏规则
- 实现的统一性:一处定义,处处生效
这套方案在实际项目中已经得到了广泛应用,不仅提高了代码质量,也大大降低了数据泄露的风险。希望这个方案能对大家有所帮助!
在数据安全越来越重要的今天,作为开发者,我们有责任保护好用户的数据隐私。这套动态脱敏方案,就是我们为数据安全加上的重要一层防护。
如果你觉得这篇文章对你有帮助,欢迎关注我们的公众号"服务端技术精选",获取更多实用的技术干货!
本文通过SpringBoot + 注解 + AOP实现了一套优雅的数据脱敏方案,让敏感信息自动脱敏,既保证了业务功能的完整性,又提升了数据安全性。
标题:SpringBoot动态脱敏实战:从注解到AOP的优雅打码术
作者:jiangyi
地址:http://jiangyi.space/articles/2026/01/06/1767667265344.html
0 评论