一、什么是数据脱敏?
数据脱敏(Data Masking),又称数据漂白、数据去隐私化或数据变形。
百度百科对数据脱敏的定义为:指对某些敏感信息通过脱敏规则进行数据的变形,实现敏感隐私数据的可靠保护。在涉及客户安全数据或者一些商业性敏感数据的情况下,在不违反系统规则条件下,对真实数据进行改造并提供测试使用,如身份证号、手机号、卡号、客户号等个人信息都需要进行数据脱敏。
生活中不乏数据脱敏的例子,比如我们最常见的火车票、电商收货人地址都会对敏感信息做处理,甚至女同志较熟悉的美颜、有些视频中的马赛克都属于脱敏。
二、为什么要进行数据脱敏?
上面说到,在“涉及客户安全数据或者一些商业性敏感数据的情况下”对数据进行改造,说明我们要进行改造的数据是涉及到用户或者企业数据的安全,进行数据脱敏其实就是对这些数据进行加密,防止泄露。
对于脱敏的程度,一般来说只要处理到无法推断原有的信息,不会造成信息泄露即可,如果修改过多,容易导致丢失数据原有特性。因此,在实际操作中,需要根据实际场景来选择适当的脱敏规则。改姓名,身份证号,地址,手机号,电话号码等几个客户相关字段。
三、如何实现数据脱敏
按照脱敏规则,可以分为可恢复性脱敏和不可恢复性脱敏。可恢复性脱敏就是数据经过脱敏规则的转化后,还再次可以经过某些处理还原出原来的数据,相反,数据经过不可恢复性脱敏之后,将无法还原到原来的样子,可以把二者分别看做可逆加密和不可逆加密。
我们目前遇到的场景是日志脱敏,即在把日志中的密码,甚至姓名、身份证号等信息都进行脱敏处理。
脱敏前:
脱敏后:
如上图,仔细分析会发现,打日志之前,获得脱敏的数据就两个步骤:【拿到要输入的数据(user实体)】→【进行序列化】,所以要进行数据脱敏可以考虑在这两个步骤上进行实现。第一个方法就是在序列化实体之前先把需要脱敏的字段进行处理,之后正常序列化;第二个方法就是在实体序列化的时候,对要脱敏的字段进行处理。
后面来分享一下具体实现数据脱敏的方法。
上文说了数据过敏主要有两个思路:第一个就是在序列化实体之前先把需要脱敏的字段进行处理,之后正常序列化;第二个就是在实体序列化的时候,对要脱敏的字段进行处理。
脱敏实现思路
这里探讨第一种方法,用基于自定义注解的方式实现日志脱敏。
要对数据进行脱敏,基本上都是对一些关键的、少数字段进行脱敏,比如某个实体中可能只对password这一个字段进行脱敏处理,所以可以用自定义注解的方式,只需在需要脱敏的字段上添加一个注解,比较方便。
整体思路如下图:
写日志时,序列化之前先把要打印的对象clone一份,然后找出添加脱敏自定义注解的字段进行相应规则的处理转化(比如把“刘德华”改为“刘*华),然后再对对象进行序列化操作。
核心代码:
定义用于标识脱敏字段的注解 Desensitized.java
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Desensitized {
/*脱敏类型(规则)*/
SensitiveTypeEnum type();
/*判断注解是否生效的方法*/
String isEffictiveMethod() default "";
}
脱敏类型 SensitiveTypeEnum.java
public enum SensitiveTypeEnum {
/** 中文名 */
CHINESE_NAME,
/** 身份证号 */
ID_CARD,
/** 座机号 */
FIXED_PHONE,
/** 手机号 */
MOBILE_PHONE,
/** 地址 */
ADDRESS,
/** 电子邮件 */
EMAIL,
/** 银行卡 */
BANK_CARD,
/** 密码 */
PASSWORD;
}
实现脱敏处理类 DesensitizedUtils.java
public class DesensitizedUtils {
/**
* 获取脱敏json串
*
* @param javaBean
* @return
*/
public static String getJson(Object javaBean) {
String json = null;
if (null != javaBean) {
try {
if (javaBean.getClass().isInterface()) return json;
/* 克隆出一个实体进行字段修改,避免修改原实体 */
Object clone = ObjectUtils.deepClone(javaBean);
/* 定义一个计数器,用于避免重复循环自定义对象类型的字段 */
Set<Integer> referenceCounter = new HashSet<Integer>();
/* 对克隆实体进行脱敏操作 */
DesensitizedUtils.replace(ObjectUtils.getAllFields(clone), clone, referenceCounter);
/* 利用fastjson对脱敏后的克隆对象进行序列化 */
json = JSON.toJSONString(clone, SerializerFeature.WriteMapNullValue, SerializerFeature.WriteNullListAsEmpty);
/* 清空计数器 */
referenceCounter.clear();
referenceCounter = null;
} catch (Throwable e) {
e.printStackTrace();
}
}
return json;
}
/**
* 对需要脱敏的字段进行转化
*
* @param fields
* @param javaBean
* @param referenceCounter
* @throws IllegalArgumentException
* @throws IllegalAccessException
*/
private static void replace(Field[] fields, Object javaBean, Set<Integer> referenceCounter) throws IllegalArgumentException, IllegalAccessException {
if (null != fields && fields.length > 0) {
for (Field field : fields) {
field.setAccessible(true);
if (null != field && null != javaBean) {
Object value = field.get(javaBean);
if (null != value) {
Class<?> type = value.getClass();
//处理子属性,包括集合中的
if (type.isArray()) {//对数组类型的字段进行递归过滤
int len = Array.getLength(value);
for (int i = 0; i < len; i++) {
Object arrayObject = Array.get(value, i);
if (isNotGeneralType(arrayObject.getClass(), arrayObject, referenceCounter)) {
replace(ObjectUtils.getAllFields(arrayObject), arrayObject, referenceCounter);
}
}
} else if (value instanceof Collection<?>) {//对集合类型的字段进行递归过滤
Collection<?> c = (Collection<?>) value;
Iterator<?> it = c.iterator();
while (it.hasNext()) {
Object collectionObj = it.next();
if (isNotGeneralType(collectionObj.getClass(), collectionObj, referenceCounter)) {
replace(ObjectUtils.getAllFields(collectionObj), collectionObj, referenceCounter);
}
}
} else if (value instanceof Map<?, ?>) {//对Map类型的字段进行递归过滤
Map<?, ?> m = (Map<?, ?>) value;
Set<?> set = m.entrySet();
for (Object o : set) {
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) o;
Object mapVal = entry.getValue();
if (isNotGeneralType(mapVal.getClass(), mapVal, referenceCounter)) {
replace(ObjectUtils.getAllFields(mapVal), mapVal, referenceCounter);
}
}
} else if (value instanceof Enum<?>) {
continue;
}
/*除基础类型、jdk类型的字段之外,对其他类型的字段进行递归过滤*/
else {
if (!type.isPrimitive()
&& type.getPackage() != null
&& !StringUtils.startsWith(type.getPackage().getName(), "javax.")
&& !StringUtils.startsWith(type.getPackage().getName(), "java.")
&& !StringUtils.startsWith(field.getType().getName(), "javax.")
&& !StringUtils.startsWith(field.getName(), "java.")
&& referenceCounter.add(value.hashCode())) {
replace(ObjectUtils.getAllFields(value), value, referenceCounter);
}
}
}
//脱敏操作
setNewValueForField(javaBean, field, value);
}
}
}
}
/**
* 脱敏操作(按照规则转化需要脱敏的字段并设置新值)
* 目前只支持String类型的字段,如需要其他类型如BigDecimal、Date等类型,可以添加
*
* @param javaBean
* @param field
* @param value
* @throws IllegalAccessException
*/
public static void setNewValueForField(Object javaBean, Field field, Object value) throws IllegalAccessException {
//处理自身的属性
Desensitized annotation = field.getAnnotation(Desensitized.class);
if (field.getType().equals(String.class) && null != annotation && executeIsEffictiveMethod(javaBean, annotation)) {
String valueStr = (String) value;
if (StringUtils.isNotBlank(valueStr)) {
switch (annotation.type()) {
case CHINESE_NAME: {
field.set(javaBean, DesensitizedUtils.chineseName(valueStr));
break;
}
case ID_CARD: {
field.set(javaBean, DesensitizedUtils.idCardNum(valueStr));
break;
}
case FIXED_PHONE: {
field.set(javaBean, DesensitizedUtils.fixedPhone(valueStr));
break;
}
case MOBILE_PHONE: {
field.set(javaBean, DesensitizedUtils.mobilePhone(valueStr));
break;
}
case ADDRESS: {
field.set(javaBean, DesensitizedUtils.address(valueStr, 8));
break;
}
case EMAIL: {
field.set(javaBean, DesensitizedUtils.email(valueStr));
break;
}
case BANK_CARD: {
field.set(javaBean, DesensitizedUtils.bankCard(valueStr));
break;
}
case PASSWORD: {
field.set(javaBean, DesensitizedUtils.password(valueStr));
break;
}
}
}
}
}
}
脱敏测试对象 UserInfo.java
public class UserInfo{
@Desensitized(type = SensitiveTypeEnum.CHINESE_NAME)
private String realName;
@Desensitized(type = SensitiveTypeEnum.ID_CARD)
private String idCardNo;
@Desensitized(type = SensitiveTypeEnum.MOBILE_PHONE)
private String mobileNo;
private String account;
@Desensitized(type = SensitiveTypeEnum.PASSWORD, isEffictiveMethod = "isEffictiveMethod")
private String password;
//setter、getter略
}
测试:
@Test
public void testUserInfoDesensitize() {
BaseUserInfo baseUserInfo = new BaseUserInfo()
.setRealName("胡丹尼")
.setIdCardNo("158199199013141120")
.setMobileNo("13579246810")
.setAccount("dannyhoo123456")
.setPassword("123456");
System.out.println("脱敏前:" + JSON.toJSONString(baseUserInfo));
System.out.println("脱敏后:" + DesensitizedUtils.getJson(baseUserInfo));
}
以上仅为部分代码,全部代码已经更新到github:https://github.com/DannyHoo/desensitized
整个过程比较棘手的地方就是对象的克隆,实际场景中要打印的日志对象格式千变万化,对象的变量类型也很多,比如接口、枚举、集合、map、自定义类型等,在实现过程中也尝试了多种方法来实现实体的深克隆,比如先序列化对象,再反序列化得到克隆后的对象,或者用第三方克隆工具类,都没有很好地兼容实际环境中的对象格式,上述源码中是小编自己按照现有需求、和出现了许多错误后一遍一遍修改来的,可能会有很多不合理的地方,时间紧迫,后面继续优化。
针对整个实现的思路、实现方法,如果您有任何疑问和建议,欢迎交流讨论。如果您有更好的方法,也希望您能够分享下~
参考资料:http://blog.csdn.net/liuc0317/article/details/48787793
http://blog.csdn.net/huyuyang6688/article/details/77689459
http://blog.csdn.net/huyuyang6688/article/details/77759844