Java案例怎么实现日志脱敏?

wen java案例 72

本文目录导读:

Java案例怎么实现日志脱敏?

  1. 基于正则表达式的脱敏工具类
  2. 基于注解的脱敏方案
  3. Logback的ConversionRule实现
  4. AOP切面实现
  5. 最佳实践建议

在Java中实现日志脱敏,通常有以下几种常见方案,我会从简单到复杂,给出具体的代码案例。

基于正则表达式的脱敏工具类

这是最基础、最常用的方式,适用于Logback、Log4j等日志框架。

基础脱敏工具类

import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class DesensitizationUtil {
    /**
     * 手机号脱敏:保留前3后4,中间用****代替
     */
    public static String maskPhone(String phone) {
        if (phone == null || phone.length() < 7) {
            return phone;
        }
        return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
    }
    /**
     * 身份证号脱敏:保留前6后4
     */
    public static String maskIdCard(String idCard) {
        if (idCard == null || idCard.length() < 10) {
            return idCard;
        }
        return idCard.replaceAll("(\\d{6})\\d{8}(\\d{4})", "$1********$2");
    }
    /**
     * 邮箱脱敏:保留用户名首字母和域名
     */
    public static String maskEmail(String email) {
        if (email == null || !email.contains("@")) {
            return email;
        }
        return email.replaceAll("(\\w?)(\\w+)(@\\w+)", "$1***$3");
    }
    /**
     * 银行卡号脱敏:保留前4后4
     */
    public static String maskBankCard(String cardNo) {
        if (cardNo == null || cardNo.length() < 8) {
            return cardNo;
        }
        return cardNo.replaceAll("(\\d{4})\\d{8}(\\d{4})", "$1********$2");
    }
    /**
     * 姓名脱敏:保留姓氏,名字用*代替
     */
    public static String maskName(String name) {
        if (name == null || name.length() < 2) {
            return name;
        }
        if (name.length() == 2) {
            return name.replaceAll("(.)(.)", "$1*");
        }
        StringBuilder sb = new StringBuilder(name);
        for (int i = 1; i < sb.length(); i++) {
            sb.setCharAt(i, '*');
        }
        return sb.toString();
    }
    /**
     * 通用脱敏方法:将JSON字符串中的敏感字段替换
     */
    public static String maskSensitiveJson(String json) {
        if (json == null || json.isEmpty()) {
            return json;
        }
        // 替换手机号
        json = json.replaceAll("\"phone\"\\s*:\\s*\"(\\d{3})\\d{4}(\\d{4})\"", 
                               "\"phone\":\"$1****$2\"");
        // 替换身份证
        json = json.replaceAll("\"idCard\"\\s*:\\s*\"(\\d{6})\\d{8}(\\d{4})\"", 
                               "\"idCard\":\"$1********$2\"");
        // 替换银行卡
        json = json.replaceAll("\"bankCard\"\\s*:\\s*\"(\\d{4})\\d{8}(\\d{4})\"", 
                               "\"bankCard\":\"$1********$2\"");
        return json;
    }
}

使用示例

public class DemoService {
    private static final Logger log = LoggerFactory.getLogger(DemoService.class);
    public void processUser(User user) {
        // 手动脱敏后打印
        log.info("处理用户信息 - 姓名: {}, 手机: {}, 邮箱: {}", 
                 DesensitizationUtil.maskName(user.getName()),
                 DesensitizationUtil.maskPhone(user.getPhone()),
                 DesensitizationUtil.maskEmail(user.getEmail()));
        // 或者打印JSON时脱敏
        String userJson = JSON.toJSONString(user);
        log.info("用户信息JSON: {}", DesensitizationUtil.maskSensitiveJson(userJson));
    }
}

基于注解的脱敏方案

使用注解标记敏感字段,在序列化时自动脱敏。

自定义注解

import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SensitiveField {
    /**
     * 脱敏类型
     */
    SensitiveType type() default SensitiveType.CUSTOM;
    /**
     * 保留前几位
     */
    int keepPrefix() default 0;
    /**
     * 保留后几位
     */
    int keepSuffix() default 0;
    /**
     * 替换字符
     */
    char replaceChar() default '*';
    enum SensitiveType {
        PHONE,       // 手机号
        ID_CARD,     // 身份证
        EMAIL,       // 邮箱
        BANK_CARD,   // 银行卡
        NAME,        // 姓名
        CUSTOM       // 自定义
    }
}

脱敏序列化器

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import java.io.IOException;
import java.util.Objects;
public class SensitiveSerializer extends JsonSerializer<String> 
                                 implements ContextualSerializer {
    private SensitiveField sensitiveField;
    public SensitiveSerializer() {}
    public SensitiveSerializer(SensitiveField sensitiveField) {
        this.sensitiveField = sensitiveField;
    }
    @Override
    public void serialize(String value, JsonGenerator gen, 
                          SerializerProvider serializers) throws IOException {
        gen.writeString(mask(value));
    }
    @Override
    public JsonSerializer<?> createContextual(SerializerProvider prov, 
                                              BeanProperty property) {
        if (property != null) {
            SensitiveField annotation = property.getAnnotation(SensitiveField.class);
            if (annotation != null) {
                return new SensitiveSerializer(annotation);
            }
        }
        return prov.findValueSerializer(property.getType(), property);
    }
    private String mask(String value) {
        if (value == null || value.isEmpty()) {
            return value;
        }
        SensitiveField.SensitiveType type = sensitiveField.type();
        return switch (type) {
            case PHONE -> maskPhone(value);
            case ID_CARD -> maskIdCard(value);
            case EMAIL -> maskEmail(value);
            case BANK_CARD -> maskBankCard(value);
            case NAME -> maskName(value);
            case CUSTOM -> maskCustom(value);
        };
    }
    private String maskPhone(String phone) {
        if (phone.length() < 7) return phone;
        return phone.substring(0, 3) + "****" + phone.substring(7);
    }
    private String maskIdCard(String idCard) {
        if (idCard.length() < 10) return idCard;
        return idCard.substring(0, 6) + "********" + idCard.substring(14);
    }
    private String maskEmail(String email) {
        if (!email.contains("@")) return email;
        String[] parts = email.split("@");
        if (parts[0].length() <= 1) return email;
        return parts[0].charAt(0) + "***@" + parts[1];
    }
    private String maskBankCard(String cardNo) {
        if (cardNo.length() < 8) return cardNo;
        return cardNo.substring(0, 4) + "********" + cardNo.substring(12);
    }
    private String maskName(String name) {
        if (name.length() <= 1) return name;
        char[] chars = name.toCharArray();
        for (int i = 1; i < chars.length; i++) {
            chars[i] = '*';
        }
        return new String(chars);
    }
    private String maskCustom(String value) {
        int prefix = sensitiveField.keepPrefix();
        int suffix = sensitiveField.keepSuffix();
        char replaceChar = sensitiveField.replaceChar();
        if (prefix + suffix >= value.length()) {
            return value;
        }
        StringBuilder sb = new StringBuilder(value);
        for (int i = prefix; i < value.length() - suffix; i++) {
            sb.setCharAt(i, replaceChar);
        }
        return sb.toString();
    }
}

实体类使用

import com.fasterxml.jackson.databind.annotation.JsonSerialize;
public class User {
    @SensitiveField(type = SensitiveField.SensitiveType.NAME)
    private String name;
    @SensitiveField(type = SensitiveField.SensitiveType.PHONE)
    private String phone;
    @SensitiveField(type = SensitiveField.SensitiveType.EMAIL)
    private String email;
    @SensitiveField(type = SensitiveField.SensitiveType.ID_CARD)
    private String idCard;
    @SensitiveField(type = SensitiveField.SensitiveType.CUSTOM, 
                   keepPrefix = 4, keepSuffix = 4, replaceChar = '#')
    private String accountNo;
    // getters and setters...
    @Override
    public String toString() {
        return "User{" +
               "name='" + name + '\'' +
               ", phone='" + phone + '\'' +
               ", email='" + email + '\'' +
               ", idCard='" + idCard + '\'' +
               ", accountNo='" + accountNo + '\'' +
               '}';
    }
}

Logback的ConversionRule实现

通过自定义Logback的转换规则,实现全局自动脱敏。

自定义转换器

import ch.qos.logback.classic.pattern.MessageConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class SensitiveConverter extends MessageConverter {
    // 手机号正则
    private static final Pattern PHONE_PATTERN = 
        Pattern.compile("1[3-9]\\d{9}");
    // 身份证正则
    private static final Pattern ID_CARD_PATTERN = 
        Pattern.compile("\\d{17}[\\dxX]");
    // 邮箱正则
    private static final Pattern EMAIL_PATTERN = 
        Pattern.compile("\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*");
    @Override
    public String convert(ILoggingEvent event) {
        String message = event.getFormattedMessage();
        return maskSensitive(message);
    }
    private String maskSensitive(String message) {
        if (message == null) {
            return null;
        }
        // 手机号脱敏
        Matcher phoneMatcher = PHONE_PATTERN.matcher(message);
        while (phoneMatcher.find()) {
            String phone = phoneMatcher.group();
            message = message.replace(phone, 
                phone.substring(0, 3) + "****" + phone.substring(7));
        }
        // 身份证脱敏
        Matcher idCardMatcher = ID_CARD_PATTERN.matcher(message);
        while (idCardMatcher.find()) {
            String idCard = idCardMatcher.group();
            message = message.replace(idCard, 
                idCard.substring(0, 6) + "********" + idCard.substring(14));
        }
        // 邮箱脱敏
        Matcher emailMatcher = EMAIL_PATTERN.matcher(message);
        while (emailMatcher.find()) {
            String email = emailMatcher.group();
            String[] parts = email.split("@");
            if (parts[0].length() > 1) {
                message = message.replace(email, 
                    parts[0].charAt(0) + "***@" + parts[1]);
            }
        }
        return message;
    }
}

logback.xml配置

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 注册自定义转换器 -->
    <conversionRule conversionWord="msg" 
                    converterClass="com.example.SensitiveConverter" />
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    <root level="INFO">
        <appender-ref ref="CONSOLE" />
    </root>
</configuration>

AOP切面实现

使用Spring AOP对特定方法进行日志脱敏。

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LogDesensitizationAspect {
    private static final Logger log = LoggerFactory.getLogger(LogDesensitizationAspect.class);
    @Around("@annotation(com.example.annotation.LogDesensitization)")
    public Object aroundLogMethod(ProceedingJoinPoint joinPoint) throws Throwable {
        // 处理参数
        Object[] args = joinPoint.getArgs();
        Object[] maskedArgs = new Object[args.length];
        for (int i = 0; i < args.length; i++) {
            if (args[i] instanceof String) {
                maskedArgs[i] = maskString((String) args[i]);
            } else {
                maskedArgs[i] = maskObject(args[i]);
            }
        }
        // 打印脱敏后的参数
        log.info("调用方法: {}, 参数: {}", 
                 joinPoint.getSignature().getName(), 
                 Arrays.toString(maskedArgs));
        // 执行原方法
        Object result = joinPoint.proceed();
        // 处理返回值
        if (result != null) {
            Object maskedResult = maskObject(result);
            log.info("方法返回: {}", maskedResult);
        }
        return result;
    }
    private String maskString(String value) {
        // 对字符串进行脱敏处理
        if (value.matches("1[3-9]\\d{9}")) {
            return value.substring(0, 3) + "****" + value.substring(7);
        }
        // 其他字符串脱敏规则...
        return value;
    }
    private Object maskObject(Object obj) {
        // 使用Jackson或Gson序列化后脱敏
        if (obj != null) {
            try {
                String json = new ObjectMapper().writeValueAsString(obj);
                return DesensitizationUtil.maskSensitiveJson(json);
            } catch (Exception e) {
                return obj;
            }
        }
        return obj;
    }
}
// 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogDesensitization {
}
// 使用示例
@Service
public class UserService {
    @LogDesensitization
    public User getUserById(String userId) {
        // 方法实现...
        return user;
    }
}

最佳实践建议

配置驱动的脱敏规则

@Component
@ConfigurationProperties(prefix = "sensitive")
public class SensitiveConfig {
    private List<String> fields = new ArrayList<>();
    private Map<String, Integer> keepPrefix = new HashMap<>();
    private Map<String, Integer> keepSuffix = new HashMap<>();
    // getters and setters...
}
// application.yml
// sensitive:
//   fields:
//     - phone
//     - idCard
//     - email
//   keepPrefix:
//     phone: 3
//     idCard: 6
//     email: 1
//   keepSuffix:
//     phone: 4
//     idCard: 4
//     email: 0

性能优化建议

  1. 缓存正则表达式:避免重复编译Pattern
  2. 批量处理:对于大量日志,使用缓冲区批处理
  3. 异步处理:对日志脱敏使用异步线程处理
  4. 条件判断:只在必要的时候进行脱敏

安全建议

  1. 生产环境:必须开启日志脱敏
  2. 开发环境:可以关闭脱敏便于调试
  3. 审计日志:记录脱敏前后的转换操作
  4. 定期审查:检查是否有新的敏感字段需要脱敏

根据实际需求,你可以选择最适合的方案,建议在生产环境中至少使用前两种方案的组合,既能保证灵活性,又能保证性能。

抱歉,评论功能暂时关闭!