SpringBoot常用脱敏方案小结
引言
1.1 编写的目的
在数据脱敏功能的落地过程中,开发团队往往面临“选型难、对比杂、场景多”的困惑。目前系统梳理四种主流脱敏手段(Jackson 注解式、AOP 拦截式、DTO 手动式、数据库sql脱敏)的核心机制、优缺点及适用场景,帮助开发者在安全合规、研发效率、运行性能之间做出快速且正确的技术决策,同时规避常见的明文泄露、脏数据、性能陷阱等风险,真正做到“敏感数据看不见,业务系统照样转”。
1.2 效果示意


脱敏方式介绍
具体的脱敏方向大致分为两种
- 数据库层面
- (应用层)返回前端之前序列化处理
数据库层面脱敏:
在数据库层面进行脱敏通常意味着在 写入数据库之前 或 获取数据库数据时 对敏感数据进行处理,就像熟悉的MD5 密码加密 也是数据库脱敏的一种体现。
- 加密:对数据进行加密处理,使其在存储时不可读。
- 掩码:隐藏部分数据,如电话号码、身份证号等只显示部分信息。
数据库层面脱敏的优点是可以集中管理,并且通常更安全,因为敏感数据不会被未经处理就暴露给应用程序。
应用层面脱敏:
应用层脱敏是在数据从数据库抽出来,并且在发送给前端之前对其进行处理。这通常在业务逻辑层操作
- 脱敏对象的指定字段时调用自定义的工具类完成脱敏
3.1 Jackson + 自定义序列化器
概述
在将后端的对象返回前端之前, 会通过SpringMVC 的 默认Jackson 序列化器来实现数据的json化处理, 会根据字段类型查找对应的序列化器
利用这一扩展点,我们可以:
- 自定义一个通用脱敏序列化器(实现
JsonSerializer<String>+ContextualSerializer); - 在需要脱敏的字段上只加业务注解(如
@Desensitize(type = EMAIL)); - 序列化阶段 Jackson 自动回调该序列化器,实时将明文替换为星号,再写入 JSON。
全程零业务代码侵入,规则集中维护,性能接近原生(序列化器被缓存),是对外 API 层最轻量、最统一的脱敏方案。
没有注解的去选择默认的序列器(StringSerializer、IntSerializer...)
原理

BeanSerializer 拆包器

拓展
这种类型是以字段为粒度加注解, 那么只要在这个对象上加了注解, 就相当于把对象的字段标记了, 在哪调用都得走自定义的序列化逻辑
所以, 这里扩展一个 打标签 (标记)可以指定字段在某个接口脱敏, 不影响该字段在其他地方的正常展示
打标签的含义
因为序列化是通过getter 方法来获取字段的,所以可以在getter上做文章

addMixIn 的本质是:把“Mixin 接口/类”上的所有注解,原封不动地嫁接给目标实体类,但运行时仍然只出现目标实体类BCompany,Mixin 本身不会被实例化也不会出现在 JSON 里。
public interface CompanyDesensMixin {
@Desensitize(type = DesensitizeType.EMAIL)
String getEmail();
}3.2 AOP + 标记注解
概述
把“脱敏动作”从业务代码里抽出来,做成一个横切关注点;
在方法返回前端之前,立即调用统一脱敏处理器:通过反射按字段名遍历返回对象,将敏感字段就地替换为星号。 使用方式只用在目标字段上加一个自定义注解(包含脱敏类型),业务代码零变动,即可对任意复杂嵌套对象、集合、Map 完成脱敏。
该方案以“方法”为最小粒度,一次性配置即可让接口、日志、导出等多出口同时生效,但需承担 反射 遍历带来的 CPU 开销与循环引用风险
原理

相关代码
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Sensitive {
SensitiveType value();
}/**
* 脱敏类型枚举
*/
public enum SensitiveType {
PHONE, // 手机号
ID_CARD, // 身份证
NAME, // 姓名
EMAIL, // 邮箱
BANK_CARD // 银行卡
}/**
* 深度脱敏AOP处理器
*/
@Aspect
@Component
@Slf4j
public class DeepSensitiveAspect {
// 定义切点:拦截Controller层所有方法
@Pointcut("execution(* com.jing.springbootdemo.web.BasicController.user())")
public void controllerPointcut() {}
/**
* 环绕通知:处理Controller层返回结果
*/
@Around("controllerPointcut()")
public Object aroundController(ProceedingJoinPoint joinPoint) throws Throwable {
Object result = joinPoint.proceed();
long start = System.nanoTime();
Object res = processDeepSensitive(result);
long end = System.nanoTime();
long cost = TimeUnit.NANOSECONDS.toMillis(end - start);
log.info("Controller 通知总耗时 = {} ms | 签名 = {}", cost, joinPoint.getSignature().toShortString());
return res;
}
/**
* 递归处理单个对象所有字段
*/
private Object processObject(Object obj) {
if (obj == null) {
return null;
}
Class<?> clazz = obj.getClass();
try {
// 获取所有字段(包括父类)
List<Field> fields = getAllFields(clazz);
for (Field field : fields) {
field.setAccessible(true);
// 检查是否有脱敏注解
Sensitive sensitive = field.getAnnotation(Sensitive.class);
if (sensitive != null && field.getType() == String.class) {
// 处理敏感字段
processSensitiveField(obj, field, sensitive);
} else {
// 递归处理嵌套对象
processNestedField(obj, field);
}
}
} catch (Exception e) {
log.warn("脱敏处理失败: {}", e.getMessage());
}
return obj;
}
}这里需要写一个递归处理对象,Map,集合(反射). 如果是多字段/深度嵌套的对象, 多层反射
3.3 DTO 手动脱敏
概述
没有任何注解、没有任何框架,就是最原始的 get/set 时期的做法,最原始最灵活
代码
public UserDTO toDTO(UserEntity entity) {
UserDTO dto = new UserDTO();
// 1. 普通字段
dto.setUserName(desensitizeName(entity.getRealName()));
// 2. 身份证号
dto.setIdCard(StrUtil.hide(entity.getIdCard(), 1, 17));
// 3. 手机
dto.setMobile(DesensitizedUtil.mobilePhone(entity.getMobile()));
// 4. 邮箱
dto.setEmail(DesensitizedUtil.email(entity.getEmail()));
return dto;
}
private String desensitizeName(String fullName) {
if (fullName == null || fullName.length() < 2) return fullName;
return fullName.charAt(0) + "*" + fullName.substring(fullName.length() - 1);
}这种灵活度最高,但是如果项目多处用到数据脱敏, 就要写很多重复的代码. 且及其不好维护
3.4 数据库脱敏
概述
数据库脱敏是指在数据离开数据库之前,通过 SQL 内置函数、视图、存储过程或商业插件,对敏感字段进行实时变形,使返回给应用、报表或第三方接口的数据不再包含完整明文,从而防止泄露、满足合规的一种服务端级保护手段。
SELECT
id,
CONCAT(LEFT(phone, 3), '****', RIGHT(phone, 4)) AS phone,
CONCAT(LEFT(id_card, 6), '********', RIGHT(id_card, 4)) AS id_card,
CONCAT(LEFT(name, 1), '*', RIGHT(name, 1)) AS name,
CONCAT(LEFT(email, 4), '****', SUBSTRING(email, LOCATE('@', email))) AS email,
CONCAT(LEFT(bank_card, 4), '********', RIGHT(bank_card, 4)) AS bank_card
FROM users;通过SQL层面直接脱敏, SQL 复杂,维护成本高,无法动态控制
| 方式 | 层面 | 改动 | 性能 | 灵活度 |
| jackson 自定义注解 | 实现自定义的序列化器, 走原有springMVC的流程,无侵入脱敏只用加注解, 添加脱敏方式只需要加枚举 | 自定义 序列化器和 注解 | 走原生,不用拷贝,不用反射, 效率高 | 以字段为粒度, 也可以通过给某个 方法里的指定字段脱敏,灵活度高 |
| aop+标注注解 | 以反射的方式, 在脱敏的方法上添加脱敏注解 | 不修改原有的实体,只用在需要脱敏的方法上添加标记注解 | 以反射方式进行, 遇见嵌套对象, 需要将所有字段循环反射判断是否需要脱敏, 多字段对象,影响性能 | 以方法为粒度, 通过调用工具类来脱敏, 工具类来决定哪个字段要脱敏, 灵活度不高 |
| DTO 手动脱敏 | 代码业务层面, 需要脱敏的位置,添加 pojo -> dto 的对象转换 | 需要给需要转化的对象新建dto对象 | 无反射,纯手动调用脱敏工具,效率高 | 灵活度最高,但是需要大量修改原有 的代码和编写重复代码, 维护成本高 |
| 数据库层面 | 通过sql直接将数据解决按 | 需要脱敏的地方添加sql, 添加mapper | 每行通过sql的字符串截断方法, 不能走索引 | 灵活度差, 该字段就要改sql, 需要脱敏 就要去改sql, 复用性最低 |
性能测试
综上所述, Jackson 与 AOP 反射切入的方式 比较合理, 适用于当前业务需求;
下面写两个这两种类型的 Demo 来进行性能测试
测试结论
执行结果

可以看到Jackson的序列化性能是AOP方式的3~4倍
安全层面
jackson方式:
- 无锁、无并发问题
- 自定义的序列化器由springWeb自己管理
aop+标注注解:
- 对象如果层级嵌套多/字段多,需要手动写递归,占用大量栈空间
DTO 手动脱敏:
最原始,最安全(内存全是***,无内存残留数据)
数据库层面脱敏:
将脱敏逻辑转移到 SQL 查询出口,与业务逻辑完全解耦,依赖于SQL实现是安全等级最高的实现方式。
项目场景
对于有种情况, 在整个列表页面, 邮箱这种信息不想被一眼看到, 然后详情信息又想看见. 对于后端的实现, 用户这个数据模型肯定是一份, 不可能列表 和 详情页面各一个数据模型(对象).
这边肯定优先选择可以区分接口的方案
选择方案
jackson+自定义注解方式
- 自定义序列化处理器,springMVC 转化时(遇见自定义的注解) 直接调用自己的序列化处理器
- 只用在需要的脱敏字段上加注解
- 增加注解规则,只要更改注解工具
- 后期改字段名称,直接改名称
到此这篇关于SpringBoot常用脱敏方案小结的文章就介绍到这了,更多相关SpringBoot 常用脱敏内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
使用自定义参数解析器同一个参数支持多种Content-Type
这篇文章主要介绍了使用自定义参数解析器同一个参数支持多种Content-Type的操作,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教2021-08-08
SpringCloud通过Feign传递List类型参数方式
这篇文章主要介绍了SpringCloud通过Feign传递List类型参数方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教2022-03-03
java input 调用手机相机和本地照片上传图片到服务器然后压缩的方法
今天小编就为大家分享一篇java input 实现调用手机相机和本地照片上传图片到服务器然后压缩的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧2018-08-08


最新评论