package com.mydreamjob.audit; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; /** * 安全审计服务:以独立事务({@code REQUIRES_NEW})写入拒绝型安全事件, * 即使外层业务事务因 403/429 等异常回滚,本记录仍能落库,保证可追踪恶意举报与骚扰尝试。 * *
仅记录 userId、目标 ID、action、entityType、requestId 与时间等非敏感追踪字段, * 不记录消息正文、手机号、邮箱、密码、JWT 等隐私。
*/ @Slf4j @Service @RequiredArgsConstructor public class SecurityAuditService { private final AuditLogRepository auditLogRepository; /** * 记录一条拒绝型安全审计事件(独立事务提交,不受外层回滚影响)。 * 持久化失败不向上抛出,避免阻断主业务异常响应路径。 */ @Transactional(propagation = Propagation.REQUIRES_NEW) public void logRejected(Long userId, String action, String entityType, Long entityId, String details, AuditContext ctx) { try { String safeDetails = sanitize(details); AuditLog auditLog = AuditLog.builder() .userId(userId) .action(action) .entityType(entityType) .entityId(entityId) .details(safeDetails) .ipAddress(ctx != null ? ctx.getIpAddress() : null) .userAgent(ctx != null ? ctx.getUserAgent() : null) .requestId(ctx != null ? ctx.getRequestId() : null) .createdAt(LocalDateTime.now()) .build(); auditLogRepository.save(auditLog); } catch (Exception e) { // 安全审计持久化失败不阻断主业务异常响应 log.error("Failed to persist security audit log: action={}, userId={}, error={}", action, userId, e.getMessage()); } } /** * 脱敏:不记录可能的敏感字段值。 */ private String sanitize(String details) { if (details == null) return null; return details.replaceAll("(?i)code[=:]\\s*\\S+", "code=[REDACTED]") .replaceAll("(?i)password[=:]\\s*\\S+", "password=[REDACTED]") .replaceAll("(?i)token[=:]\\s*\\S+", "token=[REDACTED]") .replaceAll("(?i)secret[=:]\\s*\\S+", "secret=[REDACTED]") .replaceAll("(?i)phone[=:]\\s*\\S+", "phone=[REDACTED]") .replaceAll("(?i)email[=:]\\s*\\S+", "email=[REDACTED]"); } }