本文最后更新于 2025-02-21,文章内容可能已经过时。

自定义 Retrofit 日志拦截器实现敏感信息脱敏

背景

在使用 Retrofit 进行 HTTP 请求时,我们经常需要打印请求和响应日志用于调试。但是日志中可能包含敏感信息(如密码、手机号、身份证等),直接打印可能会造成安全隐患。本文将介绍如何自定义 Retrofit 日志拦截器来实现敏感信息的脱敏处理。

实现目标

  1. 合并同一请求的请求和响应日志,使日志更清晰
  2. 对敏感信息进行脱敏处理:
    • 手机号、身份证等信息:保留前后两位,中间用 * 代替
    • 密码等高敏感信息:完全用 *** 代替
  3. 不影响实际的请求和响应数据,只对日志进行脱敏

核心代码实现

1. 配置类

首先创建一个配置类来设置日志属性:

​
@Configuration
public class RetrofitConfiguration {
    @Bean
    public GlobalLogProperty globalLogProperty() {
        GlobalLogProperty property = new GlobalLogProperty();
        property.setEnable(true);
        property.setLogLevel(LogLevel.INFO);
        property.setLogStrategy(LogStrategy.BODY);
        return property;
    }
}

2. 自定义日志拦截器

创建自定义日志拦截器类:

@Component
@Slf4j
public class CustomLoggingInterceptor extends LoggingInterceptor {
    // 需要保留前后两位的字段
    private static final List<String> MASK_FIELDS = new ArrayList<>(Arrays.asList(
        "idNo", "idCard", "mobile", "phone", "bankCardNo"
        // ... 更多字段
    ));
​
    // 需要完全隐藏的字段
    private static final List<String> HIDE_FIELDS = new ArrayList<>(Arrays.asList(
        "password", "pin", "smscode"
        // ... 更多字段
    ));
​
    public CustomLoggingInterceptor(GlobalLogProperty globalLogProperty) {
        super(globalLogProperty);
    }
    
    // ... 其他实现代码
}

关键功能实现

1. 日志合并

使用 Map 存储同一线程的日志信息,确保请求和响应日志合并显示:

@Override
protected HttpLoggingInterceptor.Logger matchLogger(LogLevel level) {
    return new HttpLoggingInterceptor.Logger() {
        private final Map<String, StringBuilder> requestMap = new HashMap<>();
        private final String currentThread = Thread.currentThread().getName();
        
        @Override
        public void log(String message) {
            try {
                String logMessage = maskSensitiveInfo(message);
                StringBuilder logBuilder = requestMap.computeIfAbsent(
                    currentThread, k -> new StringBuilder()
                );
                logBuilder.append(logMessage).append("\n");
                
                // 响应结束时输出完整日志
                if (message.startsWith("<-- END")) {
                    log.info("\n{}", logBuilder.toString());
                    requestMap.remove(currentThread);
                }
            } catch (Exception e) {
                log.error("Log processing error:", e);
            }
        }
    };
}

2. 敏感信息脱敏

实现脱敏逻辑,处理不同类型的敏感信息:

private String maskSensitiveInfo(String message) {
    try {
        // 处理需要保留前后两位的字段
        for (String field : MASK_FIELDS) {
            String regex = String.format("(\"%s\"\\s*:\\s*\")([^\"]*)(\")", field);
            Pattern pattern = Pattern.compile(regex);
            Matcher matcher = pattern.matcher(message);
            
            while (matcher.find()) {
                String value = matcher.group(2);
                if (value == null || value.isEmpty()) {
                    continue;
                }
                
                // 处理不同长度的值
                if (value.length() <= 4) {
                    // 短字符串全部打码
                    message = message.replace(matcher.group(0),
                            String.format("\"%s\":\"****\"", field));
                } else {
                    // 保留前后两位
                    StringBuilder maskedValue = new StringBuilder()
                        .append(value.substring(0, 2))
                        .append("*".repeat(value.length() - 4))
                        .append(value.substring(value.length() - 2));
                        
                    message = message.replace(matcher.group(0),
                            String.format("\"%s\":\"%s\"", field, maskedValue));
                }
            }
        }
        
        // 处理需要完全隐藏的字段
        for (String field : HIDE_FIELDS) {
            // ... 类似的处理逻辑,但使用 *** 替换整个值
        }
    } catch (Exception e) {
        log.error("Mask sensitive info failed", e);
        return message;
    }
    return message;
}

使用效果

脱敏后的日志示例:

--> POST http://api.example.com/login
Content-Type: application/json
{
    "mobile": "13*******89",
    "password": "***",
    "idCard": "11**********33"
}
--> END POST
​
<-- 200 OK (136ms)
{
    "code": 0,
    "message": "success",
    "data": {
        "phone": "13*******89",
        "name": "张三"
    }
}
<-- END HTTP

注意事项

  1. 脱敏处理只影响日志输出,不会修改实际的请求和响应数据
  2. 需要及时清理日志 Map,避免内存泄漏
  3. 正则表达式的性能问题,建议预编译正则表达式
  4. 异常处理的完整性,确保日志拦截器不影响正常业务流程

总结

通过自定义 Retrofit 日志拦截器,我们实现了:

  1. 敏感信息的脱敏处理
  2. 统一的日志格式
  3. 更好的日志可读性
  4. 安全的信息展示

这个实现可以很好地平衡开发调试的便利性和信息安全的要求。您可以根据实际需求调整脱敏规则和日志格式。

​
这个教程文档包含了完整的实现思路、代码示例和注意事项。您可以根据需要进行修改和补充,添加更多的实际使用场景和注意事项。