什么是数据脱敏
数据脱敏,指的是对某些敏感信息通过脱敏规则进行数据的变形,实现敏感隐私数据的可靠保护。摘自百度百科数据脱敏。
对数据进行脱敏的操作一般是不可逆的。
脱敏内容
一般来说,脱敏内容包含但不限于各种隐私数据或商业性敏感数据,如身份证号、手机、邮箱、营业执照、银行卡号等信息,具体要求需要根据不同公司业务而定。
脱敏场景
前端页面内容
我司系统都是前后端分离的系统,脱敏方案都是在序列化层面来做,具体的实现也是基于各序列化库,如jackson、fastjson。
Jackson实现
Jackson需要自定义一个序列化器
public class JacksonDataMaskSerializer extends StdSerializer implements ContextualSerializer {
//脱敏策略枚举
private DataMaskType dataMask;
protected JacksonDataMaskSerializer() {
super(String.class);
}
@Override
public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
JacksonDataMask jacksonDataMask = property.getAnnotation(JacksonDataMask.class);
if (jacksonDataMask != null && String.class.equals(property.getType().getRawClass())){
dataMask = jacksonDataMask.maskType();
return this;
}
return prov.findContentValueSerializer(property.getType(),property);
}
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider provider) throws IOException {
//执行脱敏
String resultValue = dataMask.getStrategy().process(value,dataMask.getParams());
gen.writeString(resultValue);
}
}
由于不同字段需要使用的脱敏规则是不同的,所以直接使用@JsonSerialize(contentUsing = JacksonDataMaskSerializer.class)并没有什么意义,我们需要通过自定义Jackson的注解,来实现一个Serializer满足不同脱敏工作,自定义注解如下
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JacksonAnnotationsInside
@JsonSerialize(using = JacksonDataMaskSerializer.class)
public @interface JacksonDataMask {
/**
* 脱敏策略
*/
DataMaskType maskType() default DataMaskType.Default;
}
可以看到,使用@JacksonAnnotationsInside注解,来实现Jackson的自定义注解功能,这样即可满足不同字段的脱敏要求,使用姿势如下:
@Data
public class DemoVo{
@JacksonDataMask(maskType = DataMaskType.Phone)
private String phone;
@JacksonDataMask(maskType = DataMaskType.Mail)
private String email;
}
至于脱敏策略规则枚举,非常简单,就不写了,无非就是不同策略对字段值的部分字符替换成特殊字符,常见的如”*“;
Fastjson实现
Fastjson的实现与Jackson类似,也是自定义序列化拦截器,读取字段上注解,然后使用注解策略进行脱敏处理,具体实现略。
导出数据内容
常见导出数据的形式为导出excel,使用的导出excel工具库如easyexcel、easypoi等,此处以easyexcel为例。
我们同样需要自定义一个注解,如下:
public @interface ExcelDataMask {
/**
* 脱敏策略
*/
DataMaskType maskType() default DataMaskType.Default;
}
看起来是不是与前面介绍序列化库时自定义的注解一样,其实直接使用前面的也没问题,本质上是标志该字段的数据需要脱敏,以便不同实现的代码可以识别。
有了自定义注解后,按照Excel官方demo,并在DTO字段上进行注解。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DemoItemDto {
@ExcelProperty("手机号码")
@ExcelDataMask(maskType=DataMaskType.PHONE)
private String phone;
}
EasyExcel.write("/demo.xlsx", DemoItemDto.class).registerWriteHandler(new CellWriteHandler() {
@Override
public void afterCellDataConverted(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, WriteCellData cellData, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
if (isHead){
//head不需要脱敏
return;
}
ExcelDataMask excelDataMask = head.getField().getAnnotation(ExcelDataMask.class);
if (excelDataMask == null) {
return;
}
DataMaskType dataMaskType = excelDataMask.maskType();
if (dataMaskType == null) {
return;
}
String stringValue = cellData.getStringValue();
if (StrUtil.isNotBlank(stringValue)) {
//数据脱敏后重新写入
cellData.setStringValue(dataMaskType.process(stringValue));
}
}
}).sheet("模板").doWrite(list);
至此,一个简单的excel导出内容脱敏注解就完成了。
系统日志内容
在有严格安全规范要求公司,系统运行时打印的日志内容也是需要脱敏的。
常见的日志框架无非是logback、log4j这些(slf4j只是一个门面,不提供具体日志实现),基本上使用方法最终都是一句log.xx来实现打印。此处简单以打印json字符串为例
log.info("内容:{}",JSON.toJsonString(dto));
一般来说,有两种方案。
方案一(不推荐)
自定义dto转json字符串的方案,使用json序列化拦截器进行脱敏,这种类似方案,比较知名的实现如唯品会脱敏方案。
该方案有明显的缺点,即需对分散在代码中的所有log打印进行改造,工作量大,并且容易遗漏。
方案二
该方案是将脱敏逻辑,与业务代码剥离开,在日志框架层面进行实现。以logback为例,可以从以下两个扩展点进行实现。
自定义PatternLayout
在使用logback时,一般会自定义日志输出内容格式,使用PatternLayout来格式化,类似如下
d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
直接自定义PatternLayout对msg进行脱敏
public class DataMaskingPatternLayout extends PatternLayout {
@Override
public String doLayout(ILoggingEvent event) {
String msg = super.doLayout(event);
//字符串脱敏处理
return doMask(msg);
}
}
存在的问题
可以看到,自定义PatternLayout的性能相对来说是比较低的,所以实际项目上并不推荐该方案。
自定义Converter(推荐)
自定义PatternLayout是对格式化后的字符串进行脱敏,可拓展性较差。实际项目中,为了识别不同的日志信息后脱敏,更多的是自定义日志格式转换器Converter来实现脱敏。简单看下如何使用
//ClassicConverter是一个抽象类,是Converter的子类
public class DataMaskingConverter extends ClassicConverter {
@Override
public String convert(ILoggingEvent event) {
if (event == null) {
return null;
}
//log参数脱敏
Object[] maskArgs = argsToMask(event.getArgumentArray());
//参数脱敏后参与格式化
String msg = MessageFormatter.arrayFormat(event.getMessage(),maskArgs).getMessage();
return msg;
}
@Override
public Object[] argsToMask(Object[] argumentArray) {
if (argumentArray == null) {
return null;
}
Object[] res = new Object[argumentArray.length];
int i = 0;
for (Object arg : argumentArray) {
if (arg == null || arg instanceof Throwable) {
res[i] = arg;
continue;
}
if (ObjectUtil.isBasicType(arg)) {
if(arg instanceof String && JsonUtil.isJson(arg)){
//json字符串
res[i] = DataMask.maskJsonStr(arg);
} else {
//其他基础数据类型
res[i] = arg;
}
continue;
}
if {
//其他对象
res[i] = DataMask.toJSONString(arg);
}
i++;
}
return res;
}
}
在logback配置文件中,新增配置
可以看到,自定义Converter可以对入参的类型来选择不同的脱敏操作,相对PatternLayout来说,减少大量正则匹配,大幅提高性能。此时log.info("内容:{}",JSON.toJsonString(dto)) 需要改写成log.info("内容:{}",dto)。
但自定义Converter也存在一些问题
针对自定义Converter存在的问题,在实际项目中可以发现,如果想要单独依赖自定义Converter完全解决日志脱敏的问题,是非常困难的,因此有以下建议