public class Person { private Long id; private String name; private String phone; private String account; // setter and gettr ... }在日志脱敏之前,我们一般会这样直接打印日志
log.info("个人信息:{}",JsonUtils.toJSONString(person));然后打印完之后,日志大概是这样
个人信息:{"id":1,"name":"张无忌","phone":"17709141590","account":"14669037943256249"}那如果是这种敏感信息打印到日志中的话,安全问题是非常大的。研发人员或者其他可以访问这些敏感日志的人就可能会故意或者不小心泄露用户的个人信息,甚至干些啥坏事。
个人信息:{"id":1,"name":"**忌","phone":"177******90","account":"146*********49"}所以,很自然的,我们就写了个脱敏组件,在每一个字段上用注解来标识每一个字段是什么类型的敏感字段,需要怎么脱敏。比如,对于上面的个人信息,在打印日志的时候需要研发人员做两件事:
public class Person { // id是非敏感字段,不需要脱敏 private Long id; @Sensitive(type = SensitiveType.Name) private String name; // 堆代码 duidaima.com @Sensitive(type = SensitiveType.Phone) private String phone; @Sensitive(type = SensitiveType.Account) private String account; // setter and gettr ... }2).使用脱敏组件先脱敏,再打印日志
log.info("个人信息:{}", DataMask.toJSONString(person));具体的使用和实现原理可以参考:
Map<String,Object> personMap = new HashMap<>(); personMap.put("name","张无忌"); personMap.put("phone","17709141590"); personMap.put("account","14669037943256249"); personMap.put("id",1L);那么在配置文件中指定好对应的key的脱敏规则后就可以把Map中的敏感数据也脱敏。
#指定Map中的key为name,name,account的value的脱敏规则分别是Name,Account,Phone Name=name Account=account Phone=phone
那先不管需求是否合理吧,反正客户就是上帝,满足再说。然后,我们就开始实现了。
基本思路:复制Map,然后遍历复制后的Map,找到Key有对应脱敏规则的value,按照脱敏规则脱敏,最后使用Json框架序列化脱敏后的Map。
public class DataMask{ // other method... /** * 将需要脱敏的字段先进行脱敏操作,最后转成json格式 * @param object 需要序列化的对象 * @return 脱敏后的json格式 */ public static String toJSONString(Object object) { if (object == null) { return null; } try { // 脱敏map类型 if (object instanceof Map) { return return maskMap(object); } // 其他类型 return JsonUtil.toJSONString(object); } catch (Exception e) { return String.valueOf(object); } } private static String maskMap(Object object) { Map map = (Map) object; MaskJsonStr maskJsonStr = new MaskJsonStr(); // 复制Map HashMap<String, Object> mapClone = new HashMap<>(); mapClone.putAll(map); Map mask = maskJsonStr.maskMapValue(mapClone); return JsonUtil.getObjectMapper().writeValueAsString(mask); } } public class MaskJsonStr{ // other method... public Map<String, Object> maskMapValue(Map<String, Object> map) { for (Map.Entry<String, Object> entry : map.entrySet()) { Object val = entry.getValue(); if (val instanceof Map) { maskMapValue((Map<String, Object>) val); } else if (val instanceof Collection) { Collection collVal = maskCollection(entry.getKey(), val); map.put(entry.getKey(), collVal); } else { // 根据key从脱敏规则中获取脱敏规则,然后脱敏 Object maskVal = maskString(entry.getKey(), val); if (maskVal != null) { map.put(entry.getKey(), maskVal); } else { map.put(entry.getKey(), val); } } } return map; } }可以说,在整体思路上,没啥毛病,但是往往魔鬼就在细节中。看到这,也许有些大神,直接从代码中已经看出问题了。不急,我们还是悠着点来,给你10分钟思量一下先。好了,我知道你不会思考的,我们直接看问题。有使用我们这个组件的研发人员找过说:我c a o,你们把我的业务对象Map中的值修改掉了。
@Test public void testToJSONString() { Map<String,Object> personMap = new HashMap<>(); personMap.put("name","张无忌"); personMap.put("phone","17709141590"); personMap.put("account","14669037943256249"); personMap.put("id",1L); Map<String,Object> innerMap = new HashMap(); innerMap.put("name","张无忌的女儿"); innerMap.put("phone","18809141567"); innerMap.put("account","17869037943255678"); innerMap.put("id",2L); personMap.put("daughter",innerMap); System.out.println("脱敏后:"+DataMask.toJSONString(personMap)); System.out.println("脱敏后的原始Map对象:"+personMap); }输出结果如下:
脱敏后:
{"name":"**忌","id":1,"phone":"177*****590","daughter":{"phone":"188*****567","name":"****女儿","id":2,"account":"1***************8"},"account":"1***************9"}
脱敏后的原始Map对象:
{phone=17709141590, name=张无忌, id=1, daughter={phone=188*****567, name=****女儿, id=2, account=1***************8}, account=14669037943256249}我们发现,脱敏时是成功的,但是却把原始对象中的内嵌innerMap对象中的值修改了。要知道,作为脱敏组件,你可以有点小bug,你也可以原始简单粗暴,甚至你都可以脱敏失败(本该脱敏的却没有脱敏),但是你千万不能修改业务中使用的对象啊。
// 复制Map HashMap<String, Object> mapClone = new HashMap<>(); mapClone.putAll(map);所以,只有一层关系的简单Map是可以脱敏成功的,且不会改变原来的Map。但是对于有嵌套的Map对象时,就会修改嵌套Map对象中的值了。
从上面的分析中就可以得出其根本原因:没有正确地深度clone Map对象。那很自然地,我们的解决思路就是找到一种合适的深度 clone Map对象的方式就OK了。
然后我就问ChatGPT了,ChatGPT的回答有下面几个方法:
1.使用序列化和反序列化: 通过将对象序列化为字节流,然后再将字节流反序列化为新的对象,可以实现深度克隆。需要注意被克隆的对象及其引用类型成员变量都需要实现Serializable接口。public class MapUtil { private MapUtil() { } public static <K, V> Map<K, V> clone(Map<K, V> map) { if (map == null || map.isEmpty()) { return map; } Map cloneMap = new HashMap(); for (Map.Entry<K, V> entry : map.entrySet()) { final V value = entry.getValue(); final K key = entry.getKey(); if (value instanceof Map) { Map mapValue = (Map) value; cloneMap.put(key, clone(mapValue)); } else if (value instanceof Collection) { Collection collectionValue = (Collection) value; cloneMap.put(key, clone(collectionValue)); } else { cloneMap.put(key, value); } } return cloneMap; } public static <E> Collection<E> clone(Collection<E> collection) { if (collection == null || collection.isEmpty()) { return collection; } Collection clonedCollection; try { // 有一定的风险会反射调用失败 clonedCollection = collection.getClass().newInstance(); } catch (InstantiationException | IllegalAccessException e) { // simply deal with reflect exception throw new RuntimeException(e); } for (E e : collection) { if (e instanceof Collection) { Collection collectionE = (Collection) e; clonedCollection.add(clone(collectionE)); } else if (e instanceof Map) { Map mapE = (Map) e; clonedCollection.add(clone(mapE)); } else { clonedCollection.add(e); } } return clonedCollection; } }然后,又是一波Junit操作,嘎嘎绿灯,收拾完事。貌似,到这我们就可以下班了。但是,等等,这篇文章貌似,好像,的确,应该缺点啥吧?这尼玛不是讲的是Guava吗,到这为止,貌似跟Guava毛关系没有啊!!!
// 脱敏转换函数 public interface MaskFunction<K, V, E> { /** * @param k key * @param v value * @return 根据key和value得到脱敏(如果有需要的话)后的值 */ E mask(K k, V v); } // 自定义MaskMap对象,经过MaskFunction函数,将普通Map转换为脱敏后的Map public class MaskMap extends HashMap { private MaskFunction maskFunction; public MaskMap(MaskFunction maskFunction) { this.maskFunction = maskFunction; } @Override public Object get(Object key) { Object value = super.get(key); if (value == null) { return null; } return maskFunction.mask(key, value); } // other function to override ... }如上,Map不再是clone的玩法,而是转换的玩法,所以,这种操作是非常轻量级的。但是这种玩法也有缺点:比较麻烦,需要override好多方法(不然就需要熟读Map序列化器来找到最小化需要override的方法,并且不太靠谱),并且全部都要是转换的玩法。
终于,终于,终于,这个时候该轮到我们Guava登场了。
Maps#transformEntries(Map<K,V1>, Maps.EntryTransformer<? super K,? super V1,V2>) Returns a view of a map whose values are derived from the original map's entries. In contrast to transformValues, this method's entry-transformation logic may depend on the key as well as the value. All other properties of the transformed map, such as iteration order, are left intact.返回一个Map的试图,其中它的值是从原来map中entry派生出来的。相较于transformValues方法,这个基于entry的转换逻辑是既依赖于key又依赖于value。变换后的映射的所有其他属性(例如迭代顺序)均保持不变。这不正是我们上面需要实现的 MaskMap吗!!!除此之外,我们还需要支持,对集合类型的脱敏转换,再去看一下呢,又是惊喜!!!
public class MaskEntryTransformer implements Maps.EntryTransformer<Object, Object, Object> { private static final Maps.EntryTransformer<Object, Object, Object> MASK_ENTRY_TRANSFORMER = new MaskEntryTransformer(); private MaskEntryTransformer() { } public static Maps.EntryTransformer<Object, Object, Object> getInstance() { return MASK_ENTRY_TRANSFORMER; } @Override public Object transformEntry(Object objectKey, Object value) { if (value == null) { return null; } if (value instanceof Map) { Map valueMap = (Map) value; return Maps.transformEntries(valueMap, this); } final Maps.EntryTransformer<Object, Object, Object> thisFinalMaskEntryTransformer = this; if (value instanceof Collection) { Collection valueCollection = (Collection) value; if (valueCollection.isEmpty()) { return valueCollection; } return Collections2.transform(valueCollection, new Function<Object, Object>() { @Override public Object apply(Object input) { if (input == null) { return null; } if (input instanceof Map) { Map inputValueMap = (Map) input; return Maps.transformEntries(inputValueMap, thisFinalMaskEntryTransformer); } if (input instanceof Collection) { Collection inputValueCollection = (Collection) input; return Collections2.transform(inputValueCollection, this); } if (!(objectKey instanceof String)) { return input; } final String key = (String) objectKey; return transformPrimitiveType(key, input); } }); } if (!(objectKey instanceof String)) { return value; } final String key = (String) objectKey; return transformPrimitiveType(key, value); } /** * 按照脱敏规则脱敏基本数据类型 * * @param key * @param value * @return */ private Object transformPrimitiveType(final String key, final Object value) { // ... } }那脱敏的地方只要转换一下Map就可以了,如下:
public class DataMask { /** * 将需要脱敏的字段先进行脱敏操作,最后转成json格式 * @param object 需要序列化的对象 * @return 脱敏后的json格式 */ public static String toJSONString(Object object) { if (object == null) { return null; } try { if (object instanceof Map) { Map maskMap = Maps.transformEntries((Map) object, MaskEntryTransformer.getInstance()); return JsonUtil.toJSONString(maskMap); } return JsonUtil.toJSONString(object, jsonFilter); } catch (Exception e) { return object.toString(); } } }
public V2 get(Object key) { V1 value = fromMap.get(key); return (value != null || fromMap.containsKey(key)) ? transformer.transformEntry((K) key, value) : null; }这里的实现其实有一个细节,不知道大家注意没有。就是这一个判断:(value != null || fromMap.containsKey(key)),我们上面实现的时候直接没有考虑到值为null的情况(但value为null,但是有对应的key的时候还是应该要调用转换函数的)
public Collection<V2> values() { return new Values<K, V2>(this); }也是一样,返回一个转换后的 Collection,其中Collection中的值也是经过 transformer转换的。而对于迭代器也是一样的,都有对应的实现类把转换逻辑放进去了。
com.google.common.base.Splitter#split com.google.common.collect.Lists#partition com.google.common.collect.Sets#difference ...other这种 Veiw 的基本思想,其实就是懒加载的思想:在调用的时候不实际做转换,而是在实际使用的时候会做转换。