阿昌教你如何优雅的数据脱敏
Hi,我是阿昌
,最近有一个数据脱敏的需求,要求用户可自定义配置数据权限,并对某种类型数据进行脱敏返回给前端
一、涉及知识点
- SpringMVC
- Java反射
- Java自定义注解
- Java枚举
二、方案选择
1、需求要求
涉及主子账户权限,主账户可在权限中对一些列子账户进行数据权限配置,如姓名/年龄/身份证等,配置后子账户查询页面会根据主账户配置返回脱敏数据;
2、技术方案举例
针对如上类似需求的要求,整理出大致3类方向的技术实现思路
- 直接在代码中硬编码进行吊用脱敏服务或脱敏方案,进行数据脱敏
- 利用自定义注解的方式在SpringMVC生命周期中使用反射/拦截器postHandle等方式进行脱敏
- 利用自定义主角的方式在SpringMVC生命周期的最后JSON结果进行脱敏,用类似replace替换关键词替换字符串,实现脱敏
脱敏的具体逻辑可以直接使用hutool的轮子,如果很个性化就需要增加造轮子;
3、技术方案取舍
针对上面类似的方案进行取舍
- 思路1
- 可灵活变动;
- 但不够优雅;
业务侵入性强
,需要到处修改之前的业务代码,还可能存在修改漏了,或者代码改错的风险(不选择)
- 思路2
- 类属性转换不够灵活,
无法跨数据类型替换
,如int 替换为 str会报错,需要统一定义返回String的Vo对象; - 每次都需要反射解析,需评估
性能消耗
; - 非http场景下,DTO模型标记注解,
服务内部交互序列化脱敏问题
- 类属性转换不够灵活,
- 思路3
替换关键词遗漏
的可能,但可结合nacos进行维护配置关键词;当响应大量json时,字符串replace可能会有性能问题
;消耗内存
如果要替换的字符串较大,而原始字符串也很大,那么在替换过程中会消耗大量的内存。这可能导致内存溢出或性能下降。字符串拼接效率低下
:在替换过程中,可能需要多次拼接字符串。由于String类是不可变的,每次拼接都会创建一个新的字符串对象,这会导致效率低下;可能存在处理时间长,String类的replace方法是通过创建一个新的字符串对象来实现替换的。如果原始字符串很大,那么每次替换都需要创建一个新的字符串对象,这会导致时间复杂度较高;涉及数据安全问题
,无法保证100%替换正确
4、方案选定
上面种种都有问题,最后采用1和2方案结合的案例进行实行;
- 自定义注解;实现对某个需要脱敏字段进行标注
- 业务枚举;来控制对应脱敏逻辑的自定义实现
- 自定义序列化器,集成JsonSerializer + 实现ContextualSerializer;来整合上面的自定义注解 + 业务枚举脱敏逻辑
三、过程
1、自定义注解
1 | //作用于字段上 |
2、自定义脱敏序列化器
1 | public class SensitiveInfoSerializer extends JsonSerializer<String> implements ContextualSerializer { |
3、业务枚举
1 |
|
4、实体类
1 |
|
四、注意事项
1、自定义脱敏注解不生效
如果上文中提到的每一步都正常操作了,但自定义脱敏注解还是不生效:
那很可能是Spring Boot默认的消息转换器被替换成fastjson了,因为Spring Boot默认是使用jackson
进行序列化的,上面的方案也是
基于jackson的,但如果项目中明确指定了使用fastjson进行序列化,那上面的自定义脱敏注解就不会生效:
1 |
|
fastjson自定义序列化,此时的解决方案是新建过滤器类,实现com.alibaba.fastjson.serializer.ValueFilter接口并重写process方法:
1 | public class CustomerSensitiveValueFilter implements ValueFilter { |
然后在上面声明httpMessageConverters()的地方新增以下代码:
1 | fastJsonConfig.setSerializeFilters(new CustomerSensitiveValueFilter()); |
此时,上文中自定义的脱敏注解中,@JacksonAnnotationsInside
和@JsonSerialize(using = SensitiveInfoSerializer.class)
再次运行验证,会发现自定义脱敏注解生效了:
当时因为配置的Filter,所以的序列化操作都会进行以上反射的判断,会有性能损耗
那就可以使用下面自定义序列化器 + 自定义脱敏规则注解的方案
定义一个CustomerSensitiveSerializer实现
ObjectSerializer接口
,重写write方法实现自定义序列化逻辑1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class CustomerSensitiveSerializer implements ObjectSerializer {
public void write(JSONSerializer serializer, Object object, Object fieldName, Type fieldType, int features) throws IOException {
try {
if (fieldName != null
&& fieldName instanceof String
&& fieldType != null
&& fieldType.getTypeName() != null
&& fieldType.getTypeName() instanceof String) {
Field field = serializer.getContext().object.getClass().getDeclaredField(fieldName.toString());
Sensitive sensitive = field.getAnnotation(Sensitive.class);
if (sensitive != null) {
String desensitized = sensitive.value().desensitized(object.toString());
//todo
serializer.write(desensitized);
return;
}
}
} catch (Exception e) {
log.error("CustomerSensitiveSerializer.write出现异常,errorMsg:{}", e.getMessage(), e);
}
serializer.write(object);
}
}在需要使用自定义序列化器的属性上标记自定义脱敏规则注解
2、注意影响范围
在VO的某个字段上加上@Sensitive(type = SensitiveTypeEnum.NAME)后,所有使用到该VO的接口,在返回数据时,
该字段都会被脱敏,如果列表页接口和详情接口共用了这个VO,但实际情况是列表页该字段需要脱敏,编辑页该字段不需要脱敏,
这种场景就需要特别注意。
3、其他场景
如果有类似用于内部直接EXCEL导出等类似也需要脱敏的场景,上面就会有问题,因为是基于Springmvc的场景;
可在toJSONString方法中自定义指定Filter来走我们自定义的脱敏逻辑Filter;
1 | String s1 = JSON.toJSONString(obj,new CustomerSensitiveValueFilter()); |
参考内容: