Java日志完整性案例

wen java案例 3

Java日志完整性案例

背景与问题

在企业应用中,日志系统是审计和故障排查的关键。日志完整性指确保日志记录不可被篡改、删除或伪造,满足合规要求(如PCI DSS、SOX等)。

Java日志完整性案例

常见风险

  • 攻击者删除或修改敏感操作日志
  • 日志注入(伪造日志条目)
  • 日志文件被恶意覆盖

完整日志系统架构

graph TB
    A[应用系统] --> B[日志收集器]
    B --> C[日志签名模块]
    C --> D[加密存储]
    D --> E[日志审计]
    subgraph 完整性保护
        C1[HMAC签名] --> C
        C2[时间戳绑定] --> C
        C3[链条哈希] --> C
    end
    subgraph 验证
        E1[签名验证] --> E
        E2[完整性校验] --> E
        E3[异常检测] --> E
    end

核心实现方案

1 基于HMAC的日志完整性

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.Base64;
public class SecureLogger {
    private static final String HMAC_ALGORITHM = "HmacSHA256";
    private final Mac hmac;
    private byte[] previousHash = new byte[32]; // 初始化为0
    public SecureLogger(String secretKey) throws Exception {
        SecretKeySpec keySpec = new SecretKeySpec(
            secretKey.getBytes(), HMAC_ALGORITHM
        );
        this.hmac = Mac.getInstance(HMAC_ALGORITHM);
        this.hmac.init(keySpec);
    }
    public String createSecureLog(String message) {
        try {
            long timestamp = Instant.now().toEpochMilli();
            // 构建日志条目
            String logEntry = String.format(
                "TIMESTAMP=%d|MSG=%s|PREV_HASH=%s",
                timestamp, 
                message, 
                bytesToHex(previousHash)
            );
            // 计算签名
            byte[] signature = hmac.doFinal(logEntry.getBytes());
            String signatureStr = Base64.getEncoder().encodeToString(signature);
            // 更新前一个哈希(形成链条)
            previousHash = MessageDigest.getInstance("SHA-256")
                .digest(signature);
            return String.format(
                "%s|SIGNATURE=%s\n", 
                logEntry, 
                signatureStr
            );
        } catch (Exception e) {
            throw new RuntimeException("日志签名失败", e);
        }
    }
    private String bytesToHex(byte[] bytes) {
        StringBuilder result = new StringBuilder();
        for (byte b : bytes) {
            result.append(String.format("%02x", b));
        }
        return result.toString();
    }
}

2 日志验证器

public class LogVerifier {
    private final Mac hmac;
    private byte[] expectedPreviousHash = new byte[32];
    public LogVerifier(String secretKey) throws Exception {
        SecretKeySpec keySpec = new SecretKeySpec(
            secretKey.getBytes(), "HmacSHA256"
        );
        this.hmac = Mac.getInstance("HmacSHA256");
        this.hmac.init(keySpec);
    }
    public boolean verifyLogLine(String logLine) {
        try {
            // 解析日志行
            String[] parts = logLine.split("\\|");
            String timestamp = parts[0].split("=")[1];
            String message = parts[1].split("=", 2)[1];
            String prevHash = parts[2].split("=")[1];
            String signature = parts[3].split("=")[1];
            // 验证前一个哈希
            if (!prevHash.equals(bytesToHex(expectedPreviousHash))) {
                System.err.println("哈希链断裂! 日志可能被篡改");
                return false;
            }
            // 重建日志条目
            String logEntry = String.format(
                "TIMESTAMP=%s|MSG=%s|PREV_HASH=%s",
                timestamp, message, prevHash
            );
            // 验证签名
            byte[] expectedSignature = hmac.doFinal(logEntry.getBytes());
            String expectedSigStr = Base64.getEncoder()
                .encodeToString(expectedSignature);
            if (!signature.equals(expectedSigStr)) {
                System.err.println("签名不匹配! 日志内容被修改");
                return false;
            }
            // 更新预期哈希
            expectedPreviousHash = MessageDigest.getInstance("SHA-256")
                .digest(expectedSignature);
            return true;
        } catch (Exception e) {
            System.err.println("日志验证失败: " + e.getMessage());
            return false;
        }
    }
}

3 防注入示例

public class LogSanitizer {
    // 防止日志注入(CRLF注入、特殊字符)
    public static String sanitizeLogMessage(String message) {
        if (message == null) return "";
        // 替换危险字符
        String sanitized = message
            .replaceAll("[\r\n]", "")  // 移除换行
            .replaceAll("\\|", "\\|")    // 转义分隔符
            .replaceAll("=", "\\=")      // 转义等号
            .replaceAll("[\\x00-\\x1F]", "")  // 移除控制字符
            .trim();
        // 限制长度
        if (sanitized.length() > 1000) {
            sanitized = sanitized.substring(0, 997) + "...";
        }
        return sanitized;
    }
}

企业级实现

1 使用Elasticsearch + 区块链式存储

@Component
public class EnterpriseLogger {
    @Autowired
    private ElasticsearchRestTemplate elasticsearchTemplate;
    private final String INDEX_PREFIX = "secure-logs-";
    public void logSecureEvent(LogEvent event) {
        // 1. 清理日志内容
        event.setMessage(LogSanitizer.sanitizeLogMessage(event.getMessage()));
        // 2. 生成签名
        String logEntry = buildLogEntry(event);
        String signature = generateSignature(logEntry);
        event.setSignature(signature);
        // 3. 获取前一个日志的哈希
        String previousHash = getPreviousLogHash();
        event.setPreviousHash(previousHash);
        // 4. 计算当前日志哈希
        String currentHash = calculateHash(logEntry + signature + previousHash);
        event.setCurrentHash(currentHash);
        // 5. 存储到ES
        IndexQuery indexQuery = new IndexQueryBuilder()
            .withIndexName(INDEX_PREFIX + LocalDate.now())
            .withId(event.getLogId())
            .withObject(event)
            .build();
        elasticsearchTemplate.index(indexQuery, IndexCoordinates.of(INDEX_PREFIX + "*"));
    }
    private String getPreviousLogHash() {
        // 查询最新的日志哈希
        NativeSearchQuery query = new NativeSearchQueryBuilder()
            .withQuery(QueryBuilders.matchAllQuery())
            .withSort(SortBuilders.fieldSort("timestamp").order(SortOrder.DESC))
            .withPageable(PageRequest.of(0, 1))
            .build();
        SearchHits<LogEvent> hits = elasticsearchTemplate.search(
            query, LogEvent.class
        );
        return hits.hasSearchHits() ? 
               hits.getSearchHit(0).getContent().getCurrentHash() : 
               "0000000000000000000000000000000000000000000000000000000000000000";
    }
    private String calculateHash(String data) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hash = digest.digest(data.getBytes(StandardCharsets.UTF_8));
            return bytesToHex(hash);
        } catch (Exception e) {
            throw new RuntimeException("哈希计算失败", e);
        }
    }
}

2 完整性监控

@Component
public class LogIntegrityMonitor {
    @Scheduled(cron = "0 0 * * * *") // 每小时执行
    public void verifyLogIntegrity() {
        List<LogEvent> logs = fetchLogsFromLastHour();
        for (int i = 1; i < logs.size(); i++) {
            LogEvent current = logs.get(i);
            LogEvent previous = logs.get(i - 1);
            // 验证链条
            if (!current.getPreviousHash().equals(previous.getCurrentHash())) {
                alertSecurityTeam(
                    "日志完整性被破坏!", 
                    current.getLogId()
                );
            }
            // 验证签名
            String expectedSignature = generateSignature(
                buildLogEntry(current)
            );
            if (!current.getSignature().equals(expectedSignature)) {
                alertSecurityTeam(
                    "日志签名无效!", 
                    current.getLogId()
                );
            }
        }
    }
}

测试案例

@SpringBootTest
public class LogIntegrityTest {
    @Test
    public void testLogCreationAndVerification() throws Exception {
        // 创建日志系统
        SecureLogger logger = new SecureLogger("my_secret_key_123");
        LogVerifier verifier = new LogVerifier("my_secret_key_123");
        // 创建并验证日志
        String log1 = logger.createSecureLog("用户登录: 张三");
        assertTrue(verifier.verifyLogLine(log1));
        String log2 = logger.createSecureLog("用户转账: 10000元");
        assertTrue(verifier.verifyLogLine(log2));
    }
    @Test
    public void testTamperedLogDetection() throws Exception {
        SecureLogger logger = new SecureLogger("my_secret_key");
        LogVerifier verifier = new LogVerifier("my_secret_key");
        String log = logger.createSecureLog("正常操作");
        // 篡改日志
        String tamperedLog = log.replace("正常操作", "恶意操作");
        assertFalse(verifier.verifyLogLine(tamperedLog));
    }
    @Test
    public void testInjectionProtection() {
        String maliciousInput = "正常操作\r\nDELETE FROM logs;--";
        String sanitized = LogSanitizer.sanitizeLogMessage(maliciousInput);
        assertFalse(sanitized.contains("\r\n"));
        assertEquals("正常操作DELETE FROM logs;--", sanitized);
    }
}

最佳实践总结

完整性保护层级

  1. 应用层:日志签名 + 哈希链条
  2. 传输层:TLS加密传输
  3. 存储层:加密存储 + 访问控制
  4. 审计层:定期完整性验证

关键配置

logging:
  secure:
    enabled: true
    algorithm: HmacSHA256
    key-rotation: 30d # 密钥轮换周期
    chain-verification: true
    injection-protection: true
    max-message-length: 1000

注意事项

  • 密钥管理使用专门的密钥管理系统(KMS)
  • 日志服务器与业务服务器分离
  • 实施最小权限原则
  • 保留原始日志作为备份
  • 定期进行恢复演练

这个方案提供了从日志创建、签名、存储到验证的完整完整性保护,确保日志的不可否认性不可篡改性

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