Java实现日志脱敏处理

Java实现日志脱敏处理

题记

在日常工作中,日志处理是我们每一个程序员必备的素质,但是在有些场景下客户信息敏感,需要进行某些字段,或者某部分字段的脱敏处理。接到需求我们开始操刀!

需求分析

处理字段的方式多种多样,如何方便,高效才是关键,众所周知在java中最好的处理方式就是封装,即,对程序员暴露出的最好是一个统一的API,不关心具体的处理逻辑,能拿到想要的返回值就好。

实现第一版

由于在RPC调用过程当中,大部分接口的参数封装数据类型都是Map,所以在此先针对Map形式实现日志脱敏功能

实现思路:

有两种实现方法:

第一种:写死配置
第二种:使用注解驱动
由于写死配置的扩展性实在是差,所以我们本次实现主要是注解驱动

定义注解

/**
 * @ClassName: DesensitizedAnnotation
 * @Description: 注解类
 * @Author: 尚先生
 * @CreateDate: 2019/1/24 17:42
 * @Version: 1.0
 */
@Target({ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface DesensitizedAnnotation {
    /*脱敏数据类型(规则)*/
    TypeEnum type();
    /*判断注解是否生效,暂时没有用到*/
    String isEffictiveMethod() default "";

}

引入枚举

主要是便于统一处理同类型的字段

public enum TypeEnum {
    /**客户名称**/
    PERSON_NAME,
    /**客户证件号**/
    PERSON_CERT_NO,
    /**客户手机号**/
    PERSON_PHONE_NO,
    /**客户银行卡名称**/
    PERSON_BANK_NAME,
    /**客户银行卡号**/
    PERSON_BANK_NO,
    /**密码**/
    PASSWORD,
}

定义基本数据模板类

主要作用是定义待过滤字段集合

/**
 * @ClassName: BaseInfo
 * @Description: 日志过滤字段基类
 * @Author: 尚先生
 * @CreateDate: 2019/1/24 17:38
 * @Version: 1.0
 */
public class BaseInfo implements Serializable {

    private static final long serialVersionUID = 1L;

    @DesensitizedAnnotation(type = TypeEnum.PERSON_NAME)
    private String custName;

    @DesensitizedAnnotation(type = TypeEnum.PERSON_CERT_NO)
    private String certNo;
}

定义处理工具类

/**
 * @ClassName: DesensitizedUtils
 * @Description: 日志脱敏工具类
 * @Author: 尚先生
 * @CreateDate: 2019/1/24 17:52
 * @Version: 1.0
 */
public class DesensitizedUtils {

    private static final Logger logger = LoggerFactory.getLogger(DesensitizedUtils.class);

    private static final Map<String, TypeEnum> annotationMaps = new HashMap<>();

    /**
     * 类加载时装配待脱敏字段
     */
    static {
        try {
            Class<?> clazz = Class.forName(BaseInfo.class.getName());
            Field[] fields = clazz.getDeclaredFields();
            for (int i = 0; i < fields.length; i++) {
                fields[i].setAccessible(true);
                DesensitizedAnnotation annotation = fields[i].getAnnotation(DesensitizedAnnotation.class);
                if (annotation != null) {
                    TypeEnum type = annotation.type();
                    String name = fields[i].getName();
                    //name为注解字段名称,value为注解类型。方便后续根据注解类型扩展
                    annotationMaps.put(name, type);
                }
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            logger.error("类加载时装配待脱敏字段异常,异常信息:[{}]", new Object[]{e});
        }
    }


    /**
     * 脱敏处理方法
     *
     * @param object
     * @return
     */
    public static String getConverent(Map<String,Object> object) {
        try {
            // 1.处理Map数据类型
            if (object instanceof Map) {
                HashMap<String, Object> reqMap = (HashMap) object;
                Iterator<String> iterator = annotationMaps.keySet().iterator();
                iterator.forEachRemaining(annotationName -> {
                    if (reqMap.keySet().contains(annotationName)) {
                        doconverentForMap(reqMap, annotationName);
                    }
                });
                return JSON.toJSONString(reqMap);
            }      
            return JSON.toJSONString(object);
        } catch (Exception e) {
            e.printStackTrace();
            logger.error("日志脱敏处理失败,回滚,详细信息:[{}]", new Object[]{e});
            return JSON.toJSONString(object);
        }
    }

    /**
     * 脱敏数据源为Map时处理方式
     *
     * @param reqMap
     * @param annotationName
     * @return
     */
    private static void doconverentForMap(HashMap<String, Object> reqMap, String annotationName) {
        String value = String.valueOf(reqMap.get(annotationName));
        if (StringUtils.isNotEmpty(value)) {
            value = doConverentByType(value, annotationName);
        }
        reqMap.put(annotationName, value);
    }


    /**
     * 根据不同注解类型处理不同字段
     *
     * @param value
     * @param annotationName
     * @return
     */
    private static String doConverentByType(String value, String annotationName) {
        TypeEnum typeEnum = annotationMaps.get(annotationName);
        switch (typeEnum) {
            case PERSON_NAME:
                value = getStringByLength(value);
                break;
            case PERSON_CERT_NO:
                value = getStringByLength(value);
            default:
                value = getStringByLength(value);
        }
        return value;
    }

    /**
     * 根据value长度取值(切分)
     *
     * @param value
     * @return
     */
    private static String getStringByLength(String value) {
        int length = value.length();
        if (length == 2){
            value = value.substring(0, 1) + "*";
        }else if (length == 3){
            value = value.substring(0,1) + "*" + value.substring(length -1);
        }else if (length > 3 && length <= 5){
            value = value.substring(0,1) + "**" + value.substring(length -2);
        }else if (length > 5 && length <= 7){
            value = value.substring(0,2) + "***" + value.substring(length -2);
        }else if (length > 7){
            value = value.substring(0,3) + "*****" + value.substring(length -3);
        }
        return value;
    }

}

定义测试类

测试第一版实现的针对Map处理的脱敏操作

/**
 * @ClassName: TestDeaensitized
 * @Description: 日志脱敏测试类
 * @Author: 尚先生
 * @CreateDate: 2019/1/24 18:27
 * @Version: 1.0
 */
public class TestDeaensitized {

    public static void main(String[] args) {
        HashMap<String, Object> hashMap = new HashMap<>();
        hashMap.put("custName", "小妮儿");
        hashMap.put("certNo", "12345678909876543");
        hashMap.put("phone", "12345678909");
        System.out.println("脱敏前:" + hashMap);
        String converent1 = DesensitizedUtils.getConverent(hashMap);
        System.out.println("脱敏后:" + converent1);
    }
}

第一版实现测试结果

针对Map实现的脱敏结果
脱敏前:{certNo=12345678909876543, phone=12345678909, custName=小妮儿}
脱敏后:{"certNo":"123*****543","phone":"12345678909","custName":"小*儿"}

至此第一版功能实现顺利完成。

实现第二版

由于在RPC调用过程当中,大部分接口的参数封装数据类型都是Map,但是部分接口还是使用Java Bean所以在此针对Java Bean形式实现日志脱敏功能

实现思路:

根据不同的数据类型进行不同判断,屏蔽上层调用者的可见度,在底层动态实现分情况处理
在结果处理完之后,统一返回调用者序列化完成的数据信息

在第一版实现的基础之上,我们开始第二版的实现

添加实体类

主要是为了封装模拟RPC调用过程中参数实体的属性

/**
 * @ClassName: Person
 * @Description: Person实体类
 * @Author: 尚先生
 * @CreateDate: 2019/1/24 17:50
 * @Version: 1.0
 */
public class Person {

    private String custName;

    private int idNo;

    private String certNo;

    public String getCustName() {
        return custName;
    }

    public void setCustName(String custName) {
        this.custName = custName;
    }

    public int getIdNo() {
        return idNo;
    }

    public void setIdNo(int idNo) {
        this.idNo = idNo;
    }

    public String getCertNo() {
        return certNo;
    }

    public void setCertNo(String certNo) {
        this.certNo = certNo;
    }

    @Override
    public String toString() {
        return "Person{" +
                "custName='" + custName + '\'' +
                ", idNo=" + idNo +
                ", certNo='" + certNo + '\'' +
                '}';
    }
}

改造处理工具类

/**
 * @ClassName: DesensitizedUtils
 * @Description: 日志脱敏工具类
 * @Author: 尚先生
 * @CreateDate: 2019/1/24 17:52
 * @Version: 1.0
 */
public class DesensitizedUtils {

    private static final Logger logger = LoggerFactory.getLogger(DesensitizedUtils.class);

    private static final Map<String, TypeEnum> annotationMaps = new HashMap<>();

    /**
     * 类加载时装配待脱敏字段
     */
    static {
        try {
            Class<?> clazz = Class.forName(BaseInfo.class.getName());
            Field[] fields = clazz.getDeclaredFields();
            for (int i = 0; i < fields.length; i++) {
                fields[i].setAccessible(true);
                DesensitizedAnnotation annotation = fields[i].getAnnotation(DesensitizedAnnotation.class);
                if (annotation != null) {
                    TypeEnum type = annotation.type();
                    String name = fields[i].getName();
                    //name为注解字段名称,value为注解类型。方便后续根据注解类型扩展
                    annotationMaps.put(name, type);
                }
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            logger.error("类加载时装配待脱敏字段异常,异常信息:[{}]", new Object[]{e});
        }
    }


    /**
     * 脱敏处理方法
     *
     * @param object
     * @return
     */
    public static String getConverent(Object object) {

        String objClassName = object.getClass().getName();

        try {
            // 1.处理Map数据类型
            if (object instanceof Map) {
                HashMap<String, Object> reqMap = (HashMap) object;
                Iterator<String> iterator = annotationMaps.keySet().iterator();
                iterator.forEachRemaining(annotationName -> {
                    if (reqMap.keySet().contains(annotationName)) {
                        doconverentForMap(reqMap, annotationName);
                    }
                });
                return JSON.toJSONString(reqMap);
            }
            // 2.处理Object数据类型
            Object val = new Object();
            Class<?> objClazz = Class.forName(objClassName);
            Field[] declaredFields = objClazz.getDeclaredFields();
            for (int j = 0; j < declaredFields.length; j++) {
                Iterator<String> iterator = annotationMaps.keySet().iterator();
                while (iterator.hasNext()) {
                    String annotationName = iterator.next();
                    if (declaredFields[j].getName().equals(annotationName)) {
                        declaredFields[j].setAccessible(true);
                        val = declaredFields[j].get(object);
                        //获取属性后现在默认处理的是String类型,其他类型数据可扩展
                        String value = doconverentForObject(val, annotationName);
                        declaredFields[j].set(object, value);
                    }
                }
            }
            return JSON.toJSONString(object);
        } catch (Exception e) {
            e.printStackTrace();
            logger.error("日志脱敏处理失败,回滚,详细信息:[{}]", new Object[]{e});
            return JSON.toJSONString(object);
        }
    }

    /**
     * 脱敏数据源为Object时处理方式
     *
     * @param val
     * @param annotationName
     * @return
     */
    private static String doconverentForObject(Object val, String annotationName) {

        String value = String.valueOf(val);
        if (StringUtils.isNotEmpty(value)) {
            value = doConverentByType(value, annotationName);
        }
        return value;
    }

    /**
     * 脱敏数据源为Map时处理方式
     *
     * @param reqMap
     * @param annotationName
     * @return
     */
    private static void doconverentForMap(HashMap<String, Object> reqMap, String annotationName) {
        String value = String.valueOf(reqMap.get(annotationName));
        if (StringUtils.isNotEmpty(value)) {
            value = doConverentByType(value, annotationName);
        }
        reqMap.put(annotationName, value);
    }


    /**
     * 根据不同注解类型处理不同字段
     *
     * @param value
     * @param annotationName
     * @return
     */
    private static String doConverentByType(String value, String annotationName) {
        TypeEnum typeEnum = annotationMaps.get(annotationName);
        switch (typeEnum) {
            case PERSON_NAME:
                value = getStringByLength(value);
                break;
            case PERSON_CERT_NO:
                value = getStringByLength(value);
            default:
                value = getStringByLength(value);
        }
        return value;
    }

    /**
     * 根据value长度取值(切分)
     *
     * @param value
     * @return
     */
    private static String getStringByLength(String value) {
        int length = value.length();
        if (length == 2){
            value = value.substring(0, 1) + "*";
        }else if (length == 3){
            value = value.substring(0,1) + "*" + value.substring(length -1);
        }else if (length > 3 && length <= 5){
            value = value.substring(0,1) + "**" + value.substring(length -2);
        }else if (length > 5 && length <= 7){
            value = value.substring(0,2) + "***" + value.substring(length -2);
        }else if (length > 7){
            value = value.substring(0,3) + "*****" + value.substring(length -3);
        }
        return value;
    }

}

定义测试类

测试第二版实现的针对Object处理的脱敏操作

/**
 * @ClassName: TestDeaensitized
 * @Description: 日志脱敏测试类
 * @Author: 尚先生
 * @CreateDate: 2019/1/24 18:27
 * @Version: 1.0
 */
public class TestDeaensitized {

    public static void main(String[] args) {
        HashMap<String, Object> hashMap = new HashMap<>();
        hashMap.put("custName", "小妮儿");
        hashMap.put("certNo", "12345678909876543");
        hashMap.put("phone", "12345678909");
        System.out.println("脱敏前:" + hashMap);
        String converent1 = DesensitizedUtils.getConverent(hashMap);
        System.out.println("脱敏后:" + converent1);
        Person person = new Person();
        person.setCertNo("12345678909876541");
        person.setCustName("小妮儿真可爱!");
        System.out.println("脱敏前:" + person);
        String converent2 = DesensitizedUtils.getConverent(person);
        System.out.println("脱敏后:" + converent2);
    }

第二版实现测试结果

针对Map实现的脱敏结果
脱敏前:{certNo=12345678909876543, phone=12345678909, custName=小妮儿}
脱敏后:{"certNo":"123*****543","phone":"12345678909","custName":"小*儿"}
针对Object实现的脱敏结果
脱敏前:Person{custName='小妮儿真可爱!', idNo=0, certNo='12345678909876541'}
脱敏后:{"certNo":"123*****541","custName":"小妮***爱!","idNo":0}

至此所有功能实现顺利完成。

完整代码和相关依赖请见GitHub

https://github.com/dwyanewede/project-learn/tree/master/src/main/java/com/learn/demo/desensitization

微信公众号

java界的小学生

猜你喜欢

转载自blog.csdn.net/shang_xs/article/details/86632071