上文说了数据过敏主要有两个思路:第一个就是在序列化实体之前先把需要脱敏的字段进行处理,之后正常序列化;第二个就是在实体序列化的时候,对要脱敏的字段进行处理。
脱敏实现思路
这里探讨第一种方法,用基于自定义注解的方式实现日志脱敏。
要对数据进行脱敏,基本上都是对一些关键的、少数字段进行脱敏,比如某个实体中可能只对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、自定义类型等,在实现过程中也尝试了多种方法来实现实体的深克隆,比如先序列化对象,再反序列化得到克隆后的对象,或者用第三方克隆工具类,都没有很好地兼容实际环境中的对象格式,上述源码中是小编自己按照现有需求、和出现了许多错误后一遍一遍修改来的,可能会有很多不合理的地方,时间紧迫,后面继续优化。
针对整个实现的思路、实现方法,如果您有任何疑问和建议,欢迎交流讨论。如果您有更好的方法,也希望您能够分享下~