本文目录导读:

在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
性能优化建议
- 缓存正则表达式:避免重复编译Pattern
- 批量处理:对于大量日志,使用缓冲区批处理
- 异步处理:对日志脱敏使用异步线程处理
- 条件判断:只在必要的时候进行脱敏
安全建议
- 生产环境:必须开启日志脱敏
- 开发环境:可以关闭脱敏便于调试
- 审计日志:记录脱敏前后的转换操作
- 定期审查:检查是否有新的敏感字段需要脱敏
根据实际需求,你可以选择最适合的方案,建议在生产环境中至少使用前两种方案的组合,既能保证灵活性,又能保证性能。