数据脱敏——基于Java自定义注解实现日志字段脱敏


  上文说了数据过敏主要有两个思路:第一个就是在序列化实体之前先把需要脱敏的字段进行处理,之后正常序列化;第二个就是在实体序列化的时候,对要脱敏的字段进行处理。
脱敏实现思路

  这里探讨第一种方法,用基于自定义注解的方式实现日志脱敏。

  要对数据进行脱敏,基本上都是对一些关键的、少数字段进行脱敏,比如某个实体中可能只对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



【 转载请注明出处——胡玉洋《数据脱敏——基于Java自定义注解实现日志字段脱敏》

  • 18
    点赞
  • 71
    收藏
    觉得还不错? 一键收藏
  • 16
    评论
对于数据库插入数据时的自动脱敏,可以通过自定义注解结合拦截器来实现。 首先,你可以定义一个注解,比如`@SensitiveData`,用于标记需要进行脱敏字段。例如: ```java @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface SensitiveData { } ``` 然后,你可以编写一个拦截器,用于在数据插入之前对带有`@SensitiveData`注解的字段进行脱敏处理。例如: ```java @Component @Aspect public class SensitiveDataInterceptor { @Autowired private SensitiveDataHandler sensitiveDataHandler; @Before("execution(* com.example.dao.*.insert*(..)) && args(data)") public void beforeInsert(Object data) throws IllegalAccessException { Field[] fields = data.getClass().getDeclaredFields(); for (Field field : fields) { if (field.isAnnotationPresent(SensitiveData.class)) { field.setAccessible(true); Object value = field.get(data); Object desensitizedValue = sensitiveDataHandler.desensitize(value); field.set(data, desensitizedValue); } } } } ``` 在上面的代码中,`SensitiveDataInterceptor`拦截了DAO层的插入操作,并在插入之前对带有`@SensitiveData`注解的字段进行脱敏处理。`SensitiveDataHandler`是一个自定义的脱敏处理器,你可以根据需求实现该类。 最后,在需要进行脱敏的实体类的字段上使用`@SensitiveData`注解即可。例如: ```java public class User { private String username; @SensitiveData private String password; // Getters and setters } ``` 这样,当向数据库插入`User`对象时,拦截器会自动对带有`@SensitiveData`注解的字段进行脱敏处理。 需要注意的是,以上代码只是一个示例,具体的实现可能根据你的项目架构和需求有所差异,可以根据实际情况进行调整。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值