日志脱敏方案

日志脱敏方案

场景:在开发环境中,往往日志中会泄露个人信息,这时候需要对日志中的个人信息做脱密处理

通过继承MessageConverter来实现

/**
 * 日志脱敏工具
 * @author wuhming
 * @date 2023-10-26
 */
@Slf4j
public class SensitiveLogDataConverter extends MessageConverter {

    // 脱敏符号 *
    public static final String SYMBOL_STAR = "*";

    /**
     * 0 加密前三位
     * 1 加密中间四位
     * 2 加密后面四位
     */
    public static Integer logMode = 1;

    /**
     * 是否开启加密规则 默认为false
     */
    public static boolean logSensitive = false;
    /**
     * 手机号
     */
    private static final Pattern PHONE_PATTERN = Pattern.compile("(?<!\\d)(1\\d{10})(?!\\d)");

    /**
     * 验证带区号的
     */
    private static final Pattern LANDLINE_AREA = Pattern.compile("[0][1-9]{2,3}-[0-9]{5,10}");

    /**
     * 验证没有区号的
     */
    private static final Pattern LANDLINE = Pattern.compile("[1-9]{1}[0-9]{5,8}");

    /**
     * 证件号码
     */
    private static final Pattern ID_NUMBER = Pattern.compile("((1[1-5])|(2[1-3])|(3[1-7])|(4[1-6])|(5[0-4])|(6[1-5])|71|81|82|91)[0-9]{4}(18|19|20)[0-9]{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)[0-9]{3}[0-9Xx]");


    /**
     * 银行卡
     */
    //private static final Pattern BANK_CARD_NUMBER = Pattern.compile("^(?:62|88|99|19\\d{2}|4\\d{3}|5[1-5]\\d{2}|6\\d{3}|81\\d{2})\\d{10}|^62\\d{12,17}|^[0-9]{16,19}$");

    /**
     * 银行卡
     */
    private static final Pattern BANK_CARD_NUMBER = Pattern.compile("[0-9]{16,19}");
    /**
     * 邮箱符号
     */
    private static final Pattern EMAIL = Pattern.compile("\\w[-\\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\\.)+[A-Za-z]{2,14}");

    //private static final Pattern ADDRESS = Pattern.compile("/^([\\u4e00-\\u9fa5]+(?:省|市|自治区)){0,1}([\\u4e00-\\u9fa5]+(?:市|区|县|州|盟)){0,1}([\\u4e00-\\u9fa5]+(?:街道|镇|乡)){0,1}([\\u4e00-\\u9fa5]+(?:号|村|社)){0,1}([\\u4e00-\\u9fa5]+(?:路|街|巷)){0,1}([\\u4e00-\\u9fa5]+(?:弄|号楼|栋|单元)){0,1}([0-9A-Za-z]{5,}){0,1}$/");

    //
    private static final Pattern ADDRESS = Pattern.compile("([^区]+区)|([^镇]+镇)|([^路]+路)|([^街]+街)|([^单元]+单元)|([^公司]+公司)|([^室]+室)|([^户]+户)");

    static {
        Object value = null;
        try {
            value = YmlReadUtil.getValue("application.yml", "","logging.enable-sensitive");
        } catch (Exception e) {
            e.printStackTrace();
        }
        if(value != null){
            if(value instanceof Boolean){
                logSensitive = (Boolean) value;
            }else if(value instanceof String){
                logSensitive = Boolean.parseBoolean((String) value);
            }
        }
    }

    @Override
    public String convert(ILoggingEvent event) {
        String logMsg = event.getFormattedMessage();
        if (logSensitive) {
            try {

                final Set<String> email = validateEmail(logMsg);
                if (!email.isEmpty()) {
                    for (String param : email) {
                        logMsg = logMsg.replaceAll(param, emailEncrypt(param));
                    }
                }

                final Set<String> phoneNumber = validatePhoneNumber(logMsg);
                //电话号码加密
                if (!phoneNumber.isEmpty()) {
                    for (String param : phoneNumber) {
                        logMsg = logMsg.replaceAll(param, mobileEncrypt(param));
                    }
                }

                final Set<String> landlineArea = validateLandlineArea(logMsg);
                if (!phoneNumber.isEmpty()) {
                    for (String param : landlineArea) {
                        logMsg = logMsg.replaceAll(param, idNumberEncrypt(param));
                    }
                }

                final Set<String> IDNumber = validateIDNumber(logMsg);
                if (!IDNumber.isEmpty()) {
                    for (String param : IDNumber) {
                        logMsg = logMsg.replaceAll(param, idNumberEncrypt(param));
                    }
                }
                final Set<String> cardNumber = validateCardNumber(logMsg);
                if (!cardNumber.isEmpty()) {
                    for (String param : cardNumber) {
                        logMsg = logMsg.replaceAll(param, idNumberEncrypt(param));
                    }
                }
                final Set<String> bankCardNumber = validateBankCardNumber(logMsg);
                if (!bankCardNumber.isEmpty()) {
                    for (String param : bankCardNumber) {
                        logMsg = logMsg.replaceAll(param, idNumberEncrypt(param));
                    }
                }

                logMsg = dealAddress(logMsg);
                return logMsg;
            } catch (Exception e) {
                e.printStackTrace();
                return super.convert(event);
            }
        } else {
            return logMsg;
        }
    }

    private static String dealAddress(String logMsg) {
        char[] charArray = logMsg.toCharArray();
        if (logMsg.contains("市")) {
            while (logMsg.contains("市")) {
                for (int i = logMsg.indexOf("市"); i < logMsg.length() - 1; i++) {
                    if (checkAddressEnd(charArray[i])) {
                        break;
                    }
                    charArray[i] = SYMBOL_STAR.charAt(0);
                }
                logMsg = String.valueOf(charArray);
            }
        } else {
            if (logMsg.contains("省")) {
                while (logMsg.contains("省")) {
                    for (int i = logMsg.indexOf("省"); i < logMsg.length() - 1; i++) {
                        if (checkAddressEnd(charArray[i])) {
                            break;
                        }
                        charArray[i] = SYMBOL_STAR.charAt(0);

                    }
                    logMsg = String.valueOf(charArray);
                }
            }
        }
        return String.valueOf(charArray);
    }

    private static boolean checkAddressEnd(char c) {
        return " ".equals(String.valueOf(c)) || ":".equals(String.valueOf(c)) || ";".equals(String.valueOf(c)) ||
            ":".equals(String.valueOf(c)) || ";".equals(String.valueOf(c)) || ",".equals(String.valueOf(c))
            || ",".equals(String.valueOf(c)) || "“".equals(String.valueOf(c)) || "\"".equals(String.valueOf(c));
    }

    /**
     * 获取日志字符串内容中符合手机号
     */
    private static Set<String> validatePhoneNumber(String param) {
        Set<String> set = new HashSet<>();
        // 匹配手机号
        Matcher phoneMatcher = PHONE_PATTERN.matcher(param);
        while (phoneMatcher.find()) {
            set.add(phoneMatcher.group());
        }
        return set;
    }

    private static Set<String> validateLandline(String param) {
        Set<String> set = new HashSet<>();
        // 匹配手机号
        Matcher phoneMatcher = LANDLINE.matcher(param);
        while (phoneMatcher.find()) {
            set.add(phoneMatcher.group());
        }
        return set;
    }


    private static Set<String> validateLandlineArea(String param) {
        Set<String> set = new HashSet<>();
        // 匹配手机号
        Matcher phoneMatcher = LANDLINE_AREA.matcher(param);
        while (phoneMatcher.find()) {
            set.add(phoneMatcher.group());
        }
        return set;
    }


    private static Set<String> validateIDNumber(String param) {
        Set<String> set = new HashSet<>();
        Matcher phoneMatcher = ID_NUMBER.matcher(param);
        while (phoneMatcher.find()) {
            set.add(phoneMatcher.group());
        }
        return set;
    }

    private static Set<String> validateCardNumber(String param) {
        Set<String> set = new HashSet<>();
        Matcher phoneMatcher = BANK_CARD_NUMBER.matcher(param);
        while (phoneMatcher.find()) {
            set.add(phoneMatcher.group());
        }
        return set;
    }

    private static Set<String> validateBankCardNumber(String param) {
        Set<String> set = new HashSet<>();
        Matcher phoneMatcher = BANK_CARD_NUMBER.matcher(param);
        while (phoneMatcher.find()) {
            set.add(phoneMatcher.group());
        }
        return set;
    }

    private static Set<String> validateEmail(String param) {
        Set<String> set = new HashSet<>();
        Matcher phoneMatcher = EMAIL.matcher(param);
        while (phoneMatcher.find()) {
            set.add(phoneMatcher.group());
        }
        return set;
    }

    private static Set<String> validateAddress(String param) {
        Set<String> set = new HashSet<>();
        Matcher phoneMatcher = ADDRESS.matcher(param);
        while (phoneMatcher.find()) {
            set.add(phoneMatcher.group());
        }
        return set;
    }

    /**
     * 手机号脱敏
     *
     * @param mobile
     * @return
     */
    public static String mobileEncrypt(String mobile) {
        if (StrUtil.isEmpty(mobile) || (mobile.length() != 11)) {
            return mobile;
        }
        switch (logMode) {
            case 0:
                return mobile.replaceAll("(\\d{3})(\\d{4})(\\d{4})", "***$2$3");
            case 1:
                return mobile.replaceAll("(\\d{3})(\\d{4})(\\d{4})", "$1****$3");
            case 2:
                return mobile.replaceAll("(\\d{3})(\\d{4})(\\d{4})", "$1$2****");
            default:
                return mobile;
        }
    }

    public static String idNumberEncrypt(String idNumber) {
        if (StrUtil.isEmpty(idNumber)) {
            return idNumber;
        }
        return filterString(idNumber, SYMBOL_STAR);
    }

    public static String emailEncrypt(String email) {
        if (StrUtil.isEmpty(email)) {
            return email;
        }
        return filterEmail(email, SYMBOL_STAR);
    }

    public static String addressEncrypt(String address) {
        if (StrUtil.isEmpty(address)) {
            return address;
        }
        return filterAddress(address, SYMBOL_STAR);
    }


    public static String filterAddress(String address, String symbol) {
        if (StrUtil.isNotBlank(address) && !address.contains("省") && !address.contains("市")) {
            return buildSymbol(symbol, address.length());
        }
        return address;
    }

    public static String filterEmail(String email, String symbol) {
        if (email.contains("@")) {
            int i = email.lastIndexOf("@");
            String front = email.substring(0, i);
            String rear = email.substring(i).replace("@", "#");
            return filterString(front, symbol).concat(rear);
        }
        return email;
    }

    public static String filterString(String s, String symbol) {
        if (StrUtil.isNotBlank(s)) {
            char[] charArray = s.toCharArray();
            switch (logMode) {
                //隐藏前三位
                case 0:
                    for (int i = 0; i < Math.min(3, charArray.length); i++) {
                        charArray[i] = symbol.charAt(0);
                    }
                    return String.valueOf(charArray);
                //隐藏中间4位
                case 1:
                    for (int i = Math.max((charArray.length / 2 - 2), 0); i < (Math.min((charArray.length / 2 + 2), charArray.length)); i++) {
                        charArray[i] = symbol.charAt(0);
                    }
                    return String.valueOf(charArray);
                case 2:
                    for (int i = Math.max((charArray.length - 1), 0); i >= (Math.max((charArray.length - 4), 0)); i--) {
                        charArray[i] = symbol.charAt(0);
                    }
                    return String.valueOf(charArray);
                default:
                    break;
            }
        }
        return s;
    }


    /**
     * @param symbol 符号
     * @param num    数量
     * @return 创建一定数量的相同符号字符串
     */
    public static String buildSymbol(String symbol, int num) {
        if (num < 0) {
            throw new IndexOutOfBoundsException("-1");
        }
        StringBuilder value = new StringBuilder();
        while (--num >= 0) {
            value.append(symbol);
        }
        return value.toString();
    }
    
}

修改logbak-spring.xml文件

    <!--配置脱敏转换类-->
    <conversionRule conversionWord="m" converterClass="com.example.spring.boot.test.logbak.SensitiveLogDataConverter"/>

通过配置文件:application.yml来启用脱敏

logging:
  enable-sensitive: true

测试

@Test
void test10(){
    Map<String, String> hashMap = new HashMap<>();
    hashMap.put("phone", "15692381567");
    hashMap.put("identityCard", "110001200300001234");
    hashMap.put("address", "四川省成都市天府新区");
    hashMap.put("email", "1571682356@qq.com");
    log.info("{}", JSON.toJSONString(hashMap));
}

结果

2023-11-01 17:16:19.892  INFO 75957 --- [           main] c.e.s.b.t.SpringbootTestApplicationTests test10 181  : {"address":"四川省成都****","phone":"156****1567","identityCard":"1100012****0001234","email":"157****356#qq.com"}
0%