日志备份和恢复

This commit is contained in:
xydkj 2026-04-22 17:55:39 +08:00
parent 00859fed97
commit 96d706f634
23 changed files with 1171 additions and 375 deletions

View File

@ -6,6 +6,7 @@ import com.cisd.tms.modules.log.enums.AuthLevel;
import com.cisd.tms.modules.log.enums.OperationResult;
import com.cisd.tms.modules.log.enums.OperatorRoleCode;
import com.cisd.tms.modules.log.service.OperationAuditService;
import com.cisd.tms.security.internal.InternalApiAuthInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
@ -21,10 +22,6 @@ public class OperationAuditAspect {
public static final String ATTR_ROLE_CODE = "CURRENT_ROLE_CODE";
public static final String ATTR_AUTH_LEVEL = "CURRENT_AUTH_LEVEL";
public static final String ATTR_SESSION_TOKEN = "CURRENT_SESSION_TOKEN";
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String SESSION_HEADER = "X-Session-Token";
private static final int IDLE_TIMEOUT_MINUTES = 10;
@Autowired
private OperationAuditService auditService;
@ -66,14 +63,15 @@ public class OperationAuditAspect {
*/
private void getContextInfo(OperationAuditCommand command) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
//todo 是否考虑为null的情况
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
//todo 是否考虑未知情况
Object roleObj = request.getAttribute(ATTR_ROLE_CODE);
Object roleObj = request.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE);
command.setOperatorRoleCode(OperatorRoleCode.valueOf(roleObj.toString()));
Object levelObj = request.getAttribute(ATTR_AUTH_LEVEL);
Object levelObj = request.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL);
command.setOperatorAuthLevel(AuthLevel.valueOf(levelObj.toString()));
command.setRemoteIp(request.getRemoteAddr());

View File

@ -0,0 +1,39 @@
package com.cisd.tms.modules.log.controller;
import com.cisd.tms.common.api.ApiResponse;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.modules.log.service.AuditLogRestoreService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/api/v1/audit-logs/restore")
@RequiredArgsConstructor
@Tag(name = "审计日志恢复接口", description = "将本地备份的 ZIP 文件还原并解析导入到系统中")
public class AuditLogRestoreController {
private final AuditLogRestoreService restoreService;
@PostMapping(consumes = {"multipart/form-data"})
@Operation(
summary = "导入 ZIP 恢复审计日志",
description = "上传审计日志的备份压缩包(.zip系统将进行解密、验签并落库。"
)
public ApiResponse<String> restoreFromZip(@RequestParam("file") MultipartFile file) {
if (file.isEmpty() || !file.getOriginalFilename().endsWith(".zip")) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "上传有效的 ZIP 格式备份文件");
}
String result = restoreService.auditLogRestore(file);
return ApiResponse.success(result);
}
}

View File

@ -0,0 +1,69 @@
package com.cisd.tms.modules.log.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.cisd.tms.common.api.ApiResponse;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.modules.log.dto.BackupRecordPageRequest;
import com.cisd.tms.modules.log.dto.BackupRecordResponse;
import com.cisd.tms.modules.log.entity.BackupRecordEntity;
import com.cisd.tms.modules.log.repository.BackupRecordRepository;
import com.cisd.tms.modules.log.service.AuditBackupService;
import com.cisd.tms.modules.log.service.BackupRecordService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.io.File;
@RestController
@RequestMapping("/api/v1/audit-logs/backup")
@RequiredArgsConstructor
@Tag(name = "审计日志备份接口", description = "提供审计日志的手动触发备份、备份记录分页查询以及备份文件下载功能")
public class BackupController {
private final AuditBackupService auditBackupService;
private final BackupRecordService backupRecordService;
private final BackupRecordRepository backupRecordRepository;
@PostMapping()
@Operation(summary = "触发手动备份", description = "执行一次审计日志的备份任务,生成 ZIP 备份包")
public ApiResponse<Void> backup() {
auditBackupService.manualBackup();
return ApiResponse.success();
}
@PostMapping("/record/page")
@Operation(summary = "分页查询备份记录", description = "根据条件(例如:备份类型、操作日期)分页获取历史备份记录列表")
public ApiResponse<IPage<BackupRecordResponse>> queryPage(@RequestBody BackupRecordPageRequest req) {
IPage<BackupRecordResponse> pageResult = backupRecordService.queryPage(req);
return ApiResponse.success(pageResult);
}
@GetMapping("/record/download/{recordId}")
@Operation(summary = "下载备份文件", description = "根据记录的唯一编码 (recordId) 下载对应的 ZIP 备份压缩包文件")
public ResponseEntity<Resource> downloadBackupFile(@PathVariable("recordId") String recordId) {
BackupRecordEntity record = backupRecordRepository.findByRecordId(recordId).orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "备份记录不存在"));
File file = new File(record.getFilePath());
if (!file.exists()) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "物理备份文件已被删除或丢失");
}
Resource resource = new FileSystemResource(file);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + record.getFileName() + "\"")
.contentType(MediaType.parseMediaType("application/zip"))
.contentLength(file.length())
.body(resource);
}
}

View File

@ -33,7 +33,7 @@ public class OperationAuditController {
return ApiResponse.success(pageResult);
}
@Operation(summary = "获取审计日志详情", description = "根据日志的唯一标识 ID 获取单条日志的完整信息")
@Operation(summary = "获取审计日志详情", description = "根据日志的唯一标识 LogId 获取单条日志的完整信息")
@GetMapping("/{logId}")
public ApiResponse<OperationAuditLogResponse> getDetail(@PathVariable String logId) {
OperationAuditLogResponse detail = auditService.getDetail(logId);

View File

@ -0,0 +1,23 @@
package com.cisd.tms.modules.log.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
@Data
@Schema(description = "备份记录分页查询请求参数")
public class BackupRecordPageRequest {
@Schema(description = "备份类型 (AUTO:自动 / MANUAL:手动)", allowableValues = {"AUTO", "MANUAL"}, example = "AUTO")
private String backupType;
@Schema(description = "操作日期", type = "string", format = "date", example = "2026-04-21")
private LocalDate operationDate;
@Schema(description = "当前页码", example = "1", defaultValue = "1")
private int pageNum = 1;
@Schema(description = "每页数量", example = "10", defaultValue = "10")
private int pageSize = 10;
}

View File

@ -0,0 +1,29 @@
package com.cisd.tms.modules.log.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class BackupRecordResponse {
@Schema(description = "备份记录唯一标识", example = "1bc6e347692e7b396666e78e9e491cd7")
private String recordId;
@Schema(description = "备份时间", type = "string", example = "2026-04-21 11:00:00")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime backupTime;
@Schema(description = "备份类型 (AUTO:自动 / MANUAL:手动)", example = "AUTO")
private String backupType;
@Schema(description = "备份结果 (SUCCESS:成功 / FAIL:失败)", example = "SUCCESS")
private String backupResult;
// private String fileName;
}

View File

@ -17,7 +17,7 @@ public class OperationAuditLogResponse {
@Schema(description = "操作人认证等级", example = "FULL")
private AuthLevel operatorAuthLevel;
@Schema(description = "系统模块代码", example = "LOG_MANAGEMENT")
@Schema(description = "系统模块代码", example = "DEVICE")
private ModuleCode moduleCode;
@Schema(description = "操作类型", example = "UPDATE")

View File

@ -14,33 +14,15 @@ import java.time.LocalDateTime;
@TableName("tms_backup_config")
public class BackupConfigEntity{
/**
* 主键
*/
private Integer id;
/**
* 启用状态 1:启用 0:禁用 (非空, 默认 0)
*/
private Integer enable;
/**
* Cron表达式
*/
private String cronExp;
/**
* 保留份数 (默认 1)
*/
private Integer retentionCount;
private Integer retentionCount = 1;
/**
* 上次执行时间
*/
private LocalDateTime lastBackupTime;
/**
* 其他字段的签名值 (防篡改)
*/
private String signData;
}

View File

@ -6,48 +6,28 @@ import lombok.experimental.Accessors;
import java.time.LocalDateTime;
@TableName("tms_log_backup")
@TableName("tms_log_backup_record")
@Data
@Accessors(chain = true)
public class BackupRecordEntity {
/**
* 主键
*/
private Long id;
/**
* 文件名
*/
private String recordId;
private String fileName;
/**
* 存储路径
*/
private String backupType;
private String filePath;
/**
* 文件哈希
*/
private String fileHash;
/**
* 文件签名
*/
private String fileSign;
/**
* 状态 SUCCESS/DELETED
*/
private String status;
private String BackupResult;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 其他字段签名值
*/
private String signData;
}

View File

@ -1,5 +1,5 @@
package com.cisd.tms.modules.log.enums;
public enum ModuleCode {
AUTH, INIT, UPGRADE, DEVICE, NETWORK, KEY, SYSTEM, SECURITY
AUTH, INIT, UPGRADE, DEVICE, NETWORK, KEY, SYSTEM, SECURITY, LOG
}

View File

@ -3,7 +3,12 @@ package com.cisd.tms.modules.log.mapper;
import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX;
import com.cisd.tms.modules.log.entity.OperationAuditLogEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface OperationAuditLogMapper extends BaseMapperX<OperationAuditLogEntity> {
int addBatch(@Param("list") List<OperationAuditLogEntity> entityList);
}

View File

@ -1,7 +1,13 @@
package com.cisd.tms.modules.log.repository;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.cisd.tms.modules.log.dto.BackupRecordPageRequest;
import com.cisd.tms.modules.log.entity.BackupRecordEntity;
import java.util.Optional;
public interface BackupRecordRepository {
void add(BackupRecordEntity backupRecordEntity);
IPage<BackupRecordEntity> findPage(BackupRecordPageRequest req);
Optional<BackupRecordEntity> findByRecordId(String recordId);
}

View File

@ -13,4 +13,6 @@ public interface OperationAuditLogRepository {
Optional<OperationAuditLogEntity> findByLogId(String logId);
void updateByLogId(OperationAuditLogEntity entity);
Optional<List<OperationAuditLogEntity>> findAll ();
Optional<List<String>> findExistSignValues(List<String> incomingSignValues);
int addBatch(List<OperationAuditLogEntity> auditLogEntity);
}

View File

@ -1,10 +1,19 @@
package com.cisd.tms.modules.log.repository.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.cisd.tms.modules.log.dto.BackupRecordPageRequest;
import com.cisd.tms.modules.log.entity.BackupRecordEntity;
import com.cisd.tms.modules.log.mapper.BackupRecordMapper;
import com.cisd.tms.modules.log.repository.BackupRecordRepository;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Optional;
@Repository
public class BackupRecordRepositoryImpl implements BackupRecordRepository {
@ -18,4 +27,32 @@ public class BackupRecordRepositoryImpl implements BackupRecordRepository {
public void add(BackupRecordEntity backupRecordEntity){
backupRecordMapper.insert(backupRecordEntity);
}
@Override
public IPage<BackupRecordEntity> findPage(BackupRecordPageRequest req){
IPage<BackupRecordEntity> page = new Page<>(req.getPageNum(), req.getPageSize());
LambdaQueryWrapper<BackupRecordEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(StringUtils.isNotBlank(req.getBackupType()),
BackupRecordEntity::getBackupType, req.getBackupType());
if (req.getOperationDate() != null) {
LocalDateTime startOfDay = req.getOperationDate().atStartOfDay();
LocalDateTime endOfDay = req.getOperationDate().atTime(LocalTime.MAX);
wrapper.ge(BackupRecordEntity::getCreateTime, startOfDay)
.le(BackupRecordEntity::getCreateTime, endOfDay);
}
IPage<BackupRecordEntity> entityPage = backupRecordMapper.selectPage(page, wrapper);
return entityPage;
}
@Override
public Optional<BackupRecordEntity> findByRecordId(String recordId) {
LambdaQueryWrapper<BackupRecordEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(BackupRecordEntity::getRecordId, recordId).last("LIMIT 1");;
return Optional.ofNullable(backupRecordMapper.selectOne(wrapper));
}
}

View File

@ -10,8 +10,10 @@ import com.cisd.tms.modules.log.mapper.OperationAuditLogMapper;
import com.cisd.tms.modules.log.repository.OperationAuditLogRepository;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Repository
@ -83,4 +85,35 @@ public class OperationAuditLogRepositoryImpl implements OperationAuditLogReposit
List<OperationAuditLogEntity> logList = operationAuditLogMapper.selectList(wrapper);
return Optional.ofNullable(logList);
}
@Override
public Optional<List<String>> findExistSignValues(List<String> incomingSignValues){
List<String> existList = new ArrayList<>();
int batchSize = 1000;
for (int i = 0; i < incomingSignValues.size(); i += batchSize) {
int end = Math.min(i + batchSize, incomingSignValues.size());
List<String> subList = incomingSignValues.subList(i, end);
LambdaQueryWrapper<OperationAuditLogEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.select(OperationAuditLogEntity::getSignValue)
.in(OperationAuditLogEntity::getSignValue, subList);
List<OperationAuditLogEntity> batchResult = operationAuditLogMapper.selectList(wrapper);
existList.addAll(batchResult.stream()
.map(OperationAuditLogEntity::getSignValue)
.collect(Collectors.toList()));
}
return Optional.ofNullable(existList);
}
@Override
public int addBatch(List<OperationAuditLogEntity> entityList){
int actualInserted = operationAuditLogMapper.addBatch(entityList);
return actualInserted;
}
}

View File

@ -1,64 +1,50 @@
//package com.cisd.tms.modules.log.runner;
//
//import com.cisd.tms.modules.log.dto.BackupConfigUpdateRequest;
//import com.cisd.tms.modules.log.entity.BackupConfigEntity;
//import com.cisd.tms.modules.log.repository.BackupConfigRepository;
//import com.cisd.tms.modules.log.service.AuditBackupService;
//import org.slf4j.Logger;
//import org.slf4j.LoggerFactory;
//import org.springframework.beans.factory.annotation.Autowired;
//import org.springframework.boot.CommandLineRunner;
//import org.springframework.stereotype.Component;
//
//import java.util.ArrayList;
//import java.util.List;
//
//
//@Component
//public class BackupTaskStartupRunner implements CommandLineRunner {
//
// private static final Logger log = LoggerFactory.getLogger(BackupTaskStartupRunner.class);
//
// @Autowired
// private BackupConfigRepository backupConfigRepository;
//
// @Autowired
// private AuditBackupService auditBackupService;
//
// @Override
// public void run(String... args) throws Exception {
// log.info("============= 系统启动:开始初始化审计日志备份定时任务 =============");
//
// try {
// List<BackupConfigEntity> configList = backupConfigRepository.selectAll().orElseGet(ArrayList::new);
//
// if (configList.isEmpty()) {
// log.info("未在数据库中找到备份策略配置,跳过任务初始化。");
// return;
// }
//
// for (BackupConfigEntity config : configList) {
// // 仅当状态为启用 (1) cron 表达式不为空时才去开启任务
// if (config.getEnable() != null && config.getEnable() == 1
// && config.getCronExp() != null && !config.getCronExp().isEmpty()) {
//
// BackupConfigUpdateRequest req = new BackupConfigUpdateRequest();
// req.setCronExp(config.getCronExp());
// req.setEnable(config.getEnable());
// req.setRetentionCount(config.getRetentionCount());
// auditBackupService.updateAndRestartBackupTask(
// req
// );
// log.info("成功加载并开启备份任务配置ID: {}, Cron: {}", config.getId(), config.getCronExp());
// } else {
// log.info("配置ID: {} 未启用或 Cron 为空,跳过。", config.getId());
// }
// }
//
// log.info("============= 审计日志备份定时任务初始化完成 =============");
//
// } catch (Exception e) {
// log.error("初始化审计日志备份任务发生异常: {}", e.getMessage(), e);
// }
// }
//}
package com.cisd.tms.modules.log.runner;
import com.cisd.tms.modules.log.entity.BackupConfigEntity;
import com.cisd.tms.modules.log.repository.BackupConfigRepository;
import com.cisd.tms.modules.log.service.AuditBackupService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class BackupTaskStartupRunner implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(BackupTaskStartupRunner.class);
@Autowired
private BackupConfigRepository backupConfigRepository;
@Autowired
private AuditBackupService auditBackupService;
@Override
public void run(String... args) throws Exception {
log.info("============= 系统启动:开始初始化审计日志备份定时任务 =============");
try {
BackupConfigEntity config = backupConfigRepository.find().orElseGet(BackupConfigEntity :: new);
if (config.getId() == null) {
log.info("未在数据库中找到备份策略配置,跳过任务初始化。");
return;
}
// 仅当状态为启用 (1) cron 表达式不为空时才去开启任务
if (config.getEnable() != null && config.getEnable() == 1
&& config.getCronExp() != null && !config.getCronExp().isEmpty()) {
auditBackupService.restartBackupTask(config);
log.info("成功加载并开启备份任务配置ID: {}, Cron: {}", config.getId(), config.getCronExp());
} else {
log.info("配置ID: {} 未启用或 Cron 为空,跳过。", config.getId());
}
log.info("审计日志备份定时任务初始化完成");
} catch (Exception e) {
log.error("初始化审计日志备份任务发生异常: {}", e.getMessage(), e);
}
}
}

View File

@ -1,248 +1,434 @@
//package com.cisd.tms.modules.log.service;
//
//import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult;
//import com.cisd.tms.integration.crypto.pcie.model.EccInternalEncryptRequest;
//import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
//import com.cisd.tms.modules.log.dto.BackupConfigRequest;
//import com.cisd.tms.modules.log.entity.BackupConfigEntity;
//import com.cisd.tms.modules.log.entity.BackupRecordEntity;
//import com.cisd.tms.modules.log.entity.OperationAuditLogEntity;
//import com.cisd.tms.modules.log.enums.ActionType;
//import com.cisd.tms.modules.log.enums.OperationResult;
//import com.cisd.tms.modules.log.repository.BackupConfigRepository;
//import com.cisd.tms.modules.log.repository.BackupRecordRepository;
//import com.cisd.tms.modules.log.repository.OperationAuditLogRepository;
//import org.slf4j.Logger;
//import org.slf4j.LoggerFactory;
//import org.springframework.beans.factory.annotation.Autowired;
//import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
//import org.springframework.scheduling.support.CronExpression;
//import org.springframework.scheduling.support.CronTrigger;
//import org.springframework.stereotype.Service;
//import org.springframework.transaction.annotation.Transactional;
//
//import javax.crypto.Cipher;
//import javax.crypto.spec.IvParameterSpec;
//import javax.crypto.spec.SecretKeySpec;
//import java.io.File;
//import java.io.FileOutputStream;
//import java.io.IOException;
//import java.nio.charset.StandardCharsets;
//import java.text.SimpleDateFormat;
//import java.time.LocalDateTime;
//import java.time.format.DateTimeFormatter;
//import java.util.*;
//import java.util.concurrent.ScheduledFuture;
//import java.util.zip.ZipEntry;
//import java.util.zip.ZipOutputStream;
//
//@Service
//public class AuditBackupService {
//
// private static final Logger log = LoggerFactory.getLogger(AuditBackupService.class);
//
// @Autowired
// private BackupConfigRepository backupConfigRepository;
//
// @Autowired
// private ThreadPoolTaskScheduler taskScheduler;
//
// @Autowired
// private OperationAuditLogRepository operationAuditLogRepository;
//
// @Autowired
// private BackupRecordRepository backupRecordRepository;
//
// @Autowired
// private PcieCryptoService pcieCryptoService;
// private ScheduledFuture<?> currentTaskFuture;
// private static final String PROVIDER = "BC";
//
// @Transactional(rollbackFor = Exception.class)
// public void updateAndRestartBackupTask(BackupConfigRequest req) {
// String cronExp = trim(req.getCronExp());
// if (!CronExpression.isValidExpression(req.getCronExp())) {
// throw new IllegalArgumentException("Cron表达式格式不正确请重新输入");
// }
//
// BackupConfigEntity updateEntity = new BackupConfigEntity();
// updateEntity.setEnable(req.getEnable());
// updateEntity.setCronExp(req.getCronExp());
// if (req.getRetentionCount() != null) {
// updateEntity.setRetentionCount(req.getRetentionCount());
// }
//
// backupConfigRepository.update(updateEntity);
//
//
// if (currentTaskFuture != null && !currentTaskFuture.isCancelled()) {
// currentTaskFuture.cancel(true);
// log.info("已关闭旧的备份定时任务");
// }
//
//
// if (req.getEnable() == 1) {
// currentTaskFuture = taskScheduler.schedule(
// () -> executeBackup(), // 指向下面的备份方法
// new CronTrigger(cronExp)
// );
// log.info("已开启新的备份定时任务Cron表达式: {}", cronExp);
// }
// }
//
// private void executeBackup() {
// log.info("开始执行审计日志定期备份任务");
//
// String timeStr = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
// // 定义压缩包的名称和路径
// String zipFileName = "audit_backup_" + timeStr + ".zip";
// String zipFilePath = "./data/backup/" + zipFileName;
//
// File zipFile = new File(zipFilePath);
// zipFile.getParentFile().mkdirs(); // 确保目录存在
//
// boolean isBackupSuccess = false;
// try (FileOutputStream fos = new FileOutputStream(zipFile);
// ZipOutputStream zos = new ZipOutputStream(fos)) {
//
// StringBuilder csvContent = new StringBuilder();
//
// csvContent.append("日志ID,发生时间,操作角色,认证等级,业务模块,操作动作,操作结果,来源IP,操作摘要,失败原因,审计状态,审计结果,审计人,审计时间,防篡改摘要(SM3),签名值(SM2)\n");
//
// List<OperationAuditLogEntity> logList = operationAuditLogRepository.findAll().orElseGet(ArrayList::new);
// SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//
// for (OperationAuditLogEntity auditLog : logList) {
// String summary = escapeCSV(auditLog.getSummary());
// String errorMsg = escapeCSV(auditLog.getErrorMessage());
// ActionType actionType = auditLog.getActionType();
// OperationResult result = auditLog.getOperationResult();
//
// String occurredTime = auditLog.getOccurredAt() != null ? sdf.format(auditLog.getOccurredAt()) : "";
// String auditedTime = auditLog.getAuditedAt() != null ? sdf.format(auditLog.getAuditedAt()) : "";
//
// String line = String.format("%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n",
// auditLog.getLogId(),
// occurredTime,
// auditLog.getOperatorRoleCode(),
// auditLog.getOperatorAuthLevel(),
// auditLog.getModuleCode(),
// actionType,
// result,
// auditLog.getRemoteIp(),
// summary,
// errorMsg,
// auditLog.getAuditStatus(),
// auditLog.getAuditResult(),
// auditLog.getAuditedBy(),
// auditedTime,
// auditLog.getPayloadHash(),
// auditLog.getSignValue()
// );
// csvContent.append(line);
// }
//
// byte[] orgDataBytes = csvContent.toString().getBytes(StandardCharsets.UTF_8);
//
// byte[] iv = randomBytes(16);
// byte[] sm4Key = randomBytes(16);
//
// Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS7Padding", PROVIDER);
// cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(sm4Key, "SM4"), new IvParameterSpec(iv));
//
// EccInternalEncryptRequest encryptRequest = new EccInternalEncryptRequest();
// encryptRequest.setKeyIndex(1);
// encryptRequest.setData(sm4Key);
// BackupDataResult wrappedDek = pcieCryptoService.sdfeInternalEncryptEcc(encryptRequest);
//
// Map<String, Object> envelope = new LinkedHashMap<>();
// envelope.put("version", 1);
// envelope.put("payloadAlg", "SM4-CBC");
// envelope.put("keyWrapAlg", "SDFE_InternalEncrypt_ECC");
// envelope.put("ivBase64", Base64.getEncoder().encodeToString(iv));
// envelope.put("wrappedDekBase64", Base64.getEncoder().encodeToString(normalize(wrappedDek)));
// envelope.put("dekLength", sm4Key.length);
//
//
// byte[] encryptedData = cipher.doFinal(orgDataBytes);
//
// ZipEntry csvEntry = new ZipEntry("data.csv.enc");
// zos.putNextEntry(csvEntry);
//
// zos.write(encryptedData);
// zos.closeEntry();
//
// log.info("审计日志备份完成,共备份 {} 条", logList.size());
//
// // 更新备份更新时间
// BackupConfigEntity config = new BackupConfigEntity();
// config.setLastBackupTime(LocalDateTime.now());
// backupConfigRepository.saveOrUpdate(config);
//
// isBackupSuccess = true;
//
// } catch (IOException e) {
// log.error("写入 ZIP 压缩包发生异常: {}", e.getMessage(), e);
// } catch (Exception e) {
// log.error("执行备份任务发生异常: {}", e.getMessage(), e);
// }
//
//
// if (isBackupSuccess) {
// try {
// BackupRecordEntity record = new BackupRecordEntity()
// .setFileName(zipFileName)
// .setFilePath(zipFilePath)
// .setStatus("SUCCESS");
//
// backupRecordRepository.add(record);
//
// log.info("备份记录已成功存入 tms_log_backup 表。");
// } catch (Exception e) {
// log.error("保存备份记录到数据库失败: {}", e.getMessage(), e);
// }
// }
// }
//
// private String escapeCSV(String str) {
// if (str == null) {
// return "";
// }
//
// boolean containSpecialChar = str.contains(",")
// || str.contains("\"")
// || str.contains("\n")
// || str.contains("\r");
//
// if (containSpecialChar) {
// str = str.replace("\"", "\"\"");
// str = "\"" + str + "\"";
// }
//
// return str;
// }
//
//
// private static byte[] normalize(BackupDataResult result) {
// if (result == null || result.getData() == null) {
// return new byte[0];
// }
// int safeLength = result.getLength() > 0 && result.getLength() <= result.getData().length
// ? result.getLength()
// : result.getData().length;
// byte[] normalized = new byte[safeLength];
// System.arraycopy(result.getData(), 0, normalized, 0, safeLength);
// return normalized;
// }
//
//
// private static byte[] randomBytes(int size) {
// byte[] bytes = new byte[size];
// new java.security.SecureRandom().nextBytes(bytes);
// return bytes;
// }
//
// private String trim(String str) {
// return str == null ? "" : str.trim();
// }
//}
package com.cisd.tms.modules.log.service;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult;
import com.cisd.tms.integration.crypto.pcie.model.UserKeyEncryptRequest;
import com.cisd.tms.integration.crypto.pcie.model.UserKeySignRequest;
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
import com.cisd.tms.modules.log.entity.BackupConfigEntity;
import com.cisd.tms.modules.log.entity.BackupRecordEntity;
import com.cisd.tms.modules.log.entity.OperationAuditLogEntity;
import com.cisd.tms.modules.log.repository.BackupConfigRepository;
import com.cisd.tms.modules.log.repository.BackupRecordRepository;
import com.cisd.tms.modules.log.repository.OperationAuditLogRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.Security;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.ScheduledFuture;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@Service
@RequiredArgsConstructor
public class AuditBackupService {
private static final Logger log = LoggerFactory.getLogger(AuditBackupService.class);
private final BackupConfigRepository backupConfigRepository;
private final ThreadPoolTaskScheduler taskScheduler;
private final OperationAuditLogRepository operationAuditLogRepository;
private final BackupRecordRepository backupRecordRepository;
private final PcieCryptoService pcieCryptoService;
private final ObjectMapper objectMapper;
private ScheduledFuture<?> currentTaskFuture;
private static final String ZIPFILEPATH = "/home/tms/audit-logs/backup/";
private static final String PROVIDER = "BC";
static {
if (Security.getProvider(PROVIDER) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
@Transactional(rollbackFor = Exception.class)
public void restartBackupTask(BackupConfigEntity entity) {
String cronExp = trim(entity.getCronExp());
int enable = entity.getEnable();
if (currentTaskFuture != null && !currentTaskFuture.isCancelled()) {
currentTaskFuture.cancel(true);
log.info("已关闭旧的备份定时任务");
}
if (enable == 1) {
currentTaskFuture = taskScheduler.schedule(
() -> executeBackup(), // 指向下面的备份方法
new CronTrigger(cronExp)
);
log.info("已开启新的备份定时任务Cron表达式: {}", cronExp);
}
}
private void executeBackup() {
log.info("开始执行审计日志定期备份任务");
String timeStr = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
String zipFileName = "audit_backup_" + timeStr + ".zip";
String zipFilePath = ZIPFILEPATH + zipFileName;
File zipFile = new File(zipFilePath);
zipFile.getParentFile().mkdirs();
boolean isBackupSuccess = false;
try (FileOutputStream fos = new FileOutputStream(zipFile);
ZipOutputStream zos = new ZipOutputStream(fos)) {
StringBuilder csvContent = new StringBuilder();
csvContent.append("日志ID,发生时间,操作角色,认证等级,业务模块,操作动作,操作结果,来源IP,操作摘要,失败原因,审计状态,审计结果,审计人,审计时间,防篡改摘要(SM3),签名值(SM2),创建时间\n");
List<OperationAuditLogEntity> logList = operationAuditLogRepository.findAll().orElseGet(ArrayList::new);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
for (OperationAuditLogEntity auditLog : logList) {
String summary = escapeCSV(auditLog.getSummary());
String errorMsg = escapeCSV(auditLog.getErrorMessage());
String actionType = auditLog.getActionType() != null ? auditLog.getActionType().name() : "";
String result = auditLog.getOperationResult() != null ? auditLog.getOperationResult().name() : "";
String occurredTime = auditLog.getOccurredAt() != null ? auditLog.getOccurredAt().format(formatter) : "";
String auditedTime = auditLog.getAuditedAt() != null ? auditLog.getAuditedAt().format(formatter) : "";
String createdTime = auditLog.getCreateTime() != null ? auditLog.getCreateTime().format(formatter) : "";
String line = String.format("%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n",
auditLog.getLogId(),
occurredTime,
auditLog.getOperatorRoleCode() != null ? auditLog.getOperatorRoleCode() : "",
auditLog.getOperatorAuthLevel() != null ? auditLog.getOperatorAuthLevel() : "",
auditLog.getModuleCode(),
actionType,
result,
auditLog.getRemoteIp(),
summary,
errorMsg,
auditLog.getAuditStatus() != null ? auditLog.getAuditStatus() : "",
auditLog.getAuditResult() != null ? auditLog.getAuditResult() : "",
auditLog.getAuditedBy()!= null ? auditLog.getAuditedBy() : "",
auditedTime,
auditLog.getPayloadHash(),
auditLog.getSignValue(),
createdTime
);
csvContent.append(line);
}
byte[] orgDataBytes = csvContent.toString().getBytes(StandardCharsets.UTF_8);
byte[] iv = randomBytes(16);
byte[] sm4Key = randomBytes(16);
Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS7Padding", "BC"); // 假设使用 BouncyCastle
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(sm4Key, "SM4"), new IvParameterSpec(iv));
byte[] encryptedData = cipher.doFinal(orgDataBytes);
UserKeyEncryptRequest encryptRequest = new UserKeyEncryptRequest();
encryptRequest.setKeyIndex(1);
encryptRequest.setData(sm4Key);
BackupDataResult wrappedDekResult = pcieCryptoService.userKeyEncrypt(encryptRequest);
Map<String, Object> envelope = new LinkedHashMap<>();
envelope.put("version", 1);
envelope.put("payloadAlg", "SM4-CBC");
envelope.put("keyWrapAlg", "SDFE_InternalEncrypt_ECC");
envelope.put("ivBase64", Base64.getEncoder().encodeToString(iv));
envelope.put("wrappedDekBase64", Base64.getEncoder().encodeToString(normalize(wrappedDekResult)));
envelope.put("dekLength", sm4Key.length);
byte[] envelopeBytes = objectMapper.writeValueAsBytes(envelope);
byte[] signatureBytes = sign(buildSignatureSource(envelopeBytes, encryptedData));
zos.putNextEntry(new ZipEntry("data.csv.enc"));
zos.write(encryptedData);
zos.closeEntry();
zos.putNextEntry(new ZipEntry("envelope.json"));
zos.write(envelopeBytes);
zos.closeEntry();
zos.putNextEntry(new ZipEntry("signature.sig"));
zos.write(signatureBytes);
zos.closeEntry();
log.info("审计日志备份完成,共备份 {} 条", logList.size());
BackupConfigEntity config = backupConfigRepository.find().orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "backupConfig not found"));
config.setLastBackupTime(LocalDateTime.now());
backupConfigRepository.saveOrUpdate(config);
isBackupSuccess = true;
} catch (IOException e) {
log.error("写入 ZIP 压缩包发生异常: {}", e.getMessage(), e);
} catch (Exception e) {
log.error("执行加密或备份任务发生异常: {}", e.getMessage(), e);
}
try {
BackupRecordEntity record = new BackupRecordEntity();
String recordId = UUID.randomUUID().toString().replace("-", "");
record.setRecordId(recordId);
record.setFileName(zipFileName);
record.setFilePath(zipFilePath);
record.setBackupType("AUTO");
if (isBackupSuccess) {
record.setBackupResult("SUCCESS");
} else {
record.setBackupResult("FAIL");
}
record.setCreateTime(LocalDateTime.now());
// 对生成zip文件进行哈希和签名
byte[] zipFileBytes = Files.readAllBytes(zipFile.toPath());
byte[] zipFileHash = sm3(zipFileBytes);
byte[] zipFileSign = sign(zipFileHash);
record.setFileHash(Base64.getEncoder().encodeToString(zipFileHash));
record.setFileSign(Base64.getEncoder().encodeToString(zipFileSign));
backupRecordRepository.add(record);
log.info("备份记录已成功存入 tms_log_backup 表。");
} catch (Exception e) {
log.error("保存备份记录到数据库失败: {}", e.getMessage(), e);
}
}
//手动备份
public void manualBackup() {
log.info("开始执行审计日志手动备份任务");
String timeStr = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
String zipFileName = "audit_backup_" + timeStr + ".zip";
String zipFilePath = ZIPFILEPATH + zipFileName;
File zipFile = new File(zipFilePath);
zipFile.getParentFile().mkdirs();
boolean isBackupSuccess = false;
try (FileOutputStream fos = new FileOutputStream(zipFile);
ZipOutputStream zos = new ZipOutputStream(fos)) {
StringBuilder csvContent = new StringBuilder();
csvContent.append("日志ID,发生时间,操作角色,认证等级,业务模块,操作动作,操作结果,来源IP,操作摘要,失败原因,审计状态,审计结果,审计人,审计时间,防篡改摘要(SM3),签名值(SM2),创建时间\n");
List<OperationAuditLogEntity> logList = operationAuditLogRepository.findAll().orElseGet(ArrayList::new);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
for (OperationAuditLogEntity auditLog : logList) {
String summary = escapeCSV(auditLog.getSummary());
String errorMsg = escapeCSV(auditLog.getErrorMessage());
String actionType = auditLog.getActionType() != null ? auditLog.getActionType().name() : "";
String result = auditLog.getOperationResult() != null ? auditLog.getOperationResult().name() : "";
String occurredTime = auditLog.getOccurredAt() != null ? auditLog.getOccurredAt().format(formatter) : "";
String auditedTime = auditLog.getAuditedAt() != null ? auditLog.getAuditedAt().format(formatter) : "";
String createdTime = auditLog.getCreateTime() != null ? auditLog.getCreateTime().format(formatter) : "";
String line = String.format("%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n",
auditLog.getLogId(),
occurredTime,
auditLog.getOperatorRoleCode() != null ? auditLog.getOperatorRoleCode() : "",
auditLog.getOperatorAuthLevel() != null ? auditLog.getOperatorAuthLevel() : "",
auditLog.getModuleCode(),
actionType,
result,
auditLog.getRemoteIp(),
summary,
errorMsg,
auditLog.getAuditStatus() != null ? auditLog.getAuditStatus() : "",
auditLog.getAuditResult() != null ? auditLog.getAuditResult() : "",
auditLog.getAuditedBy() != null ? auditLog.getAuditedBy() : "",
auditedTime,
auditLog.getPayloadHash(),
auditLog.getSignValue(),
createdTime
);
csvContent.append(line);
}
// 加密
byte[] orgDataBytes = csvContent.toString().getBytes(StandardCharsets.UTF_8);
byte[] iv = randomBytes(16);
byte[] sm4Key = randomBytes(16);
Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS7Padding", "BC"); // 假设使用 BouncyCastle
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(sm4Key, "SM4"), new IvParameterSpec(iv));
byte[] encryptedData = cipher.doFinal(orgDataBytes);
UserKeyEncryptRequest encryptRequest = new UserKeyEncryptRequest();
encryptRequest.setKeyIndex(1);
encryptRequest.setData(sm4Key);
BackupDataResult wrappedDekResult = pcieCryptoService.userKeyEncrypt(encryptRequest);
Map<String, Object> envelope = new LinkedHashMap<>();
envelope.put("version", 1);
envelope.put("payloadAlg", "SM4-CBC");
envelope.put("keyWrapAlg", "SDFE_InternalEncrypt_ECC");
envelope.put("ivBase64", Base64.getEncoder().encodeToString(iv));
envelope.put("wrappedDekBase64", Base64.getEncoder().encodeToString(normalize(wrappedDekResult)));
envelope.put("dekLength", sm4Key.length);
byte[] envelopeBytes = objectMapper.writeValueAsBytes(envelope);
byte[] signatureBytes = sign(buildSignatureSource(envelopeBytes, encryptedData));
zos.putNextEntry(new ZipEntry("data.csv.enc"));
zos.write(encryptedData);
zos.closeEntry();
zos.putNextEntry(new ZipEntry("envelope.json"));
zos.write(envelopeBytes);
zos.closeEntry();
zos.putNextEntry(new ZipEntry("signature.sig"));
zos.write(signatureBytes);
zos.closeEntry();
log.info("审计日志备份完成,共备份 {} 条", logList.size());
BackupConfigEntity config = backupConfigRepository.find().orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "backupConfig not found"));
config.setLastBackupTime(LocalDateTime.now());
backupConfigRepository.saveOrUpdate(config);
isBackupSuccess = true;
} catch (IOException e) {
log.error("写入 ZIP 压缩包发生异常: {}", e.getMessage(), e);
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), e.getMessage());
} catch (Exception e) {
log.error("执行加密或备份任务发生异常: {}", e.getMessage(), e);
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "执行加密或备份任务发生异常");
}
try {
BackupRecordEntity record = new BackupRecordEntity();
String recordId = UUID.randomUUID().toString().replace("-", "");
record.setRecordId(recordId);
record.setFileName(zipFileName);
record.setFilePath(zipFilePath);
record.setBackupType("MANUAL");
if (isBackupSuccess) {
record.setBackupResult("SUCCESS");
} else {
record.setBackupResult("FAIL");
}
record.setCreateTime(LocalDateTime.now());
// 对生成zip文件进行哈希和签名
byte[] zipFileBytes = Files.readAllBytes(zipFile.toPath());
byte[] zipFileHash = sm3(zipFileBytes);
byte[] zipFileSign = sign(zipFileHash);
record.setFileHash(Base64.getEncoder().encodeToString(zipFileHash));
record.setFileSign(Base64.getEncoder().encodeToString(zipFileSign));
backupRecordRepository.add(record);
log.info("备份记录已成功存入 tms_log_backup 表。");
} catch (Exception e) {
log.error("保存备份记录到数据库失败: {}", e.getMessage(), e);
}
}
static byte[] buildSignatureSource(byte[] envelopeBytes, byte[] cipherBytes) {
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
writePart(outputStream, envelopeBytes);
writePart(outputStream, cipherBytes);
return outputStream.toByteArray();
} catch (IOException ex) {
throw new IllegalStateException("failed to build package signature source", ex);
}
}
private static void writePart(ByteArrayOutputStream outputStream, byte[] data) throws IOException {
byte[] safeData = data == null ? new byte[0] : data;
outputStream.write(ByteBuffer.allocate(4).putInt(safeData.length).array());
outputStream.write(safeData);
}
private byte[] sign(byte[] dataToSign){
if (dataToSign == null) {
throw new IllegalArgumentException("dataToSign is required");
}
UserKeySignRequest request = new UserKeySignRequest();
request.setKeyIndex(1);
request.setData(sm3(dataToSign));
return normalize(pcieCryptoService.userKeySignWithSm3(request));
}
private String escapeCSV(String str) {
if (str == null) {
return "";
}
boolean containSpecialChar = str.contains(",")
|| str.contains("\"")
|| str.contains("\n")
|| str.contains("\r");
if (containSpecialChar) {
str = str.replace("\"", "\"\"");
str = "\"" + str + "\"";
}
return str;
}
private static byte[] normalize(BackupDataResult result) {
if (result == null || result.getData() == null) {
return new byte[0];
}
int safeLength = result.getLength() > 0 && result.getLength() <= result.getData().length
? result.getLength()
: result.getData().length;
byte[] normalized = new byte[safeLength];
System.arraycopy(result.getData(), 0, normalized, 0, safeLength);
return normalized;
}
private static byte[] sm3(byte[] data) {
SM3Digest digest = new SM3Digest();
digest.update(data, 0, data.length);
byte[] out = new byte[digest.getDigestSize()];
digest.doFinal(out, 0);
return out;
}
private static byte[] randomBytes(int size) {
byte[] bytes = new byte[size];
new java.security.SecureRandom().nextBytes(bytes);
return bytes;
}
private String trim(String str) {
return str == null ? "" : str.trim();
}
}

View File

@ -0,0 +1,327 @@
package com.cisd.tms.modules.log.service;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult;
import com.cisd.tms.integration.crypto.pcie.model.UserKeyDecryptRequest;
import com.cisd.tms.integration.crypto.pcie.model.UserKeyVerifyRequest;
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
import com.cisd.tms.modules.log.dto.OperationAuditCommand;
import com.cisd.tms.modules.log.entity.OperationAuditLogEntity;
import com.cisd.tms.modules.log.enums.*;
import com.cisd.tms.modules.log.repository.OperationAuditLogRepository;
import com.cisd.tms.security.internal.InternalApiAuthInterceptor;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.FileSystemUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@Slf4j
@Service
@RequiredArgsConstructor
public class AuditLogRestoreService {
private final PcieCryptoService pcieCryptoService;
private final OperationAuditLogRepository auditLogRepository;
private final OperationAuditService operationAuditService;
private final ObjectMapper objectMapper;
private static final String TEMP_RESTORE_DIR = "/home/tms/audit-logs/restore/";
@Transactional(rollbackFor = Exception.class)
public String auditLogRestore(MultipartFile file) {
String batchId = UUID.randomUUID().toString().replace("-", "");
Path extractDir = Paths.get(TEMP_RESTORE_DIR, batchId);
int totalCount = 0;
int skippedCount = 0;
int successCount = 0;
boolean restoreSuccess = false;
try {
log.info("开始审计日志恢复任务, 批次号: {}", batchId);
Files.createDirectories(extractDir);
byte[] encryptedData = null;
byte[] envelopeBytes = null;
byte[] signatureBytes = null;
try (ZipInputStream zis = new ZipInputStream(file.getInputStream())) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
zis.transferTo(baos);
if ("data.csv.enc".equals(entry.getName())) encryptedData = baos.toByteArray();
if ("envelope.json".equals(entry.getName())) envelopeBytes = baos.toByteArray();
if ("signature.sig".equals(entry.getName())) signatureBytes = baos.toByteArray();
zis.closeEntry();
}
}
if (encryptedData == null || envelopeBytes == null || signatureBytes == null) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "备份包文件不完整");
}
byte[] signatureSource = buildSignatureSource(envelopeBytes, encryptedData);
// EccInternalVerifyRequest verifyReq = new EccInternalVerifyRequest();
// verifyReq.setKeyIndex(1);
// verifyReq.setData(signatureSource);
// verifyReq.setSignature(signatureBytes);
// //todo 验签
UserKeyVerifyRequest userKeyVerifyRequest = new UserKeyVerifyRequest();
userKeyVerifyRequest.setKeyIndex(1);
userKeyVerifyRequest.setData(sm3(signatureSource));
userKeyVerifyRequest.setSignature(signatureBytes);
// try {
// pcieCryptoService.userKeyVerifyWithSm3(userKeyVerifyRequest);
// } catch (BizException ex) {
// throw ex;
// } catch (RuntimeException ex) {
// throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "signature verification failed");
// }
try {
pcieCryptoService.userKeyVerifyWithSm3(userKeyVerifyRequest);
log.info("文件验签成功");
} catch (Exception e) {
log.error("文件验签失败", e);
try {
FileSystemUtils.deleteRecursively(extractDir);
log.info("临时解压目录已清理: {}", extractDir);
} catch (IOException ioException) {
log.error("清理临时目录失败: {}", extractDir, ioException);
}
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "文件验签失败, 数据可能被篡改");
}
Map<String, Object> envelope = objectMapper.readValue(envelopeBytes, new TypeReference<Map<String, Object>>(){});
byte[] wrappedDek = Base64.getDecoder().decode(String.valueOf(envelope.get("wrappedDekBase64")));
byte[] iv = Base64.getDecoder().decode(String.valueOf(envelope.get("ivBase64")));
int dekLength = parseInt(envelope.get("dekLength"), 16);
UserKeyDecryptRequest decryptReq = new UserKeyDecryptRequest();
decryptReq.setKeyIndex(1);
decryptReq.setCipherBlob(wrappedDek);
// decryptReq.setOutBufferSize(dekLength);
byte[] sm4Key = normalize(pcieCryptoService.userKeyDecrypt(decryptReq));
Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS7Padding", "BC");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(sm4Key, "SM4"), new IvParameterSpec(iv));
byte[] decryptedCsvBytes = cipher.doFinal(encryptedData);
String csvContent = new String(decryptedCsvBytes, StandardCharsets.UTF_8);
List<OperationAuditLogEntity> pendingInsertList = new ArrayList<>();
String[] lines = csvContent.split("\n");
totalCount = lines.length - 1;
List<OperationAuditLogEntity> parsedLogs = parseCsvToEntities(lines);
List<String> incomingSignValues = parsedLogs.stream()
.map(OperationAuditLogEntity::getSignValue)
.collect(Collectors.toList());
List<String> existSignValues = auditLogRepository.findExistSignValues(incomingSignValues).orElseGet(ArrayList::new);
Set<String> existSignSet = new HashSet<>(existSignValues);
for (OperationAuditLogEntity logEntity : parsedLogs) {
if (existSignSet.contains(logEntity.getSignValue())) {
skippedCount ++; // 若已存在跳过
} else {
pendingInsertList.add(logEntity); // 若不存在加入待插入队列
}
}
// 执行批量插入
if (!pendingInsertList.isEmpty()) {
successCount = auditLogRepository.addBatch(pendingInsertList);
}
// RestoreResultResponse result = new RestoreResultResponse();
// result.setTotalCount(totalCount);
// result.setSkippedCount(skippedCount);
// result.setSuccessCount(successCount);
restoreSuccess = true;
return String.format("恢复完成。共解析 %d 条,跳过已存在 %d 条,成功恢复 %d 条",
totalCount, skippedCount, successCount);
} catch (Exception e) {
log.error("恢复审计日志失败", e);
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), e.getMessage());
} finally {
try {
FileSystemUtils.deleteRecursively(extractDir);
log.info("临时解压目录已清理: {}", extractDir);
} catch (IOException e) {
log.error("清理临时目录失败: {}", extractDir, e);
}
recordRestoreAuditLog(file.getOriginalFilename(), totalCount, skippedCount, successCount, restoreSuccess);
}
}
private static int parseInt(Object value, int defaultValue) {
if (value == null) {
return defaultValue;
}
if (value instanceof Number number) {
return number.intValue();
}
try {
return Integer.parseInt(String.valueOf(value));
} catch (NumberFormatException ex) {
return defaultValue;
}
}
private static byte[] normalize(BackupDataResult result) {
if (result == null || result.getData() == null) {
return new byte[0];
}
int safeLength = result.getLength() > 0 && result.getLength() <= result.getData().length
? result.getLength()
: result.getData().length;
byte[] normalized = new byte[safeLength];
System.arraycopy(result.getData(), 0, normalized, 0, safeLength);
return normalized;
}
static byte[] buildSignatureSource(byte[] envelopeBytes, byte[] cipherBytes) {
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
writePart(outputStream, envelopeBytes);
writePart(outputStream, cipherBytes);
return outputStream.toByteArray();
} catch (IOException ex) {
throw new IllegalStateException("failed to build package signature source", ex);
}
}
private static void writePart(ByteArrayOutputStream outputStream, byte[] data) throws IOException {
byte[] safeData = data == null ? new byte[0] : data;
outputStream.write(ByteBuffer.allocate(4).putInt(safeData.length).array());
outputStream.write(safeData);
}
//todo后续改转义
private List<OperationAuditLogEntity> parseCsvToEntities(String[] lines) {
List<OperationAuditLogEntity> list = new ArrayList<>();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
for (int i = 1; i < lines.length; i++) {
if (lines[i].trim().isEmpty()) continue;
String[] cols = lines[i].split(",", -1);
OperationAuditLogEntity entity = new OperationAuditLogEntity();
entity.setLogId(cols[0]);
if (!cols[1].isEmpty()) {
entity.setOccurredAt(LocalDateTime.parse(cols[1], formatter));
}
if (!cols[2].isEmpty()) entity.setOperatorRoleCode(OperatorRoleCode.valueOf(cols[2]));
if (!cols[3].isEmpty()) entity.setOperatorAuthLevel(AuthLevel.valueOf(cols[3]));
if (!cols[4].isEmpty()) entity.setModuleCode(ModuleCode.valueOf(cols[4]));
if (!cols[5].isEmpty()) entity.setActionType(ActionType.valueOf(cols[5]));
if (!cols[6].isEmpty()) entity.setOperationResult(OperationResult.valueOf(cols[6]));
entity.setRemoteIp(cols[7]);
entity.setSummary(cols[8].replace("\"\"", "\"").replaceAll("^\"|\"$", ""));
entity.setErrorMessage(cols[9].replace("\"\"", "\"").replaceAll("^\"|\"$", ""));
if (!cols[10].isEmpty()) entity.setAuditStatus(AuditStatus.valueOf(cols[10]));
if (!cols[11].isEmpty()) entity.setAuditResult(AuditResult.valueOf(cols[11]));
entity.setAuditedBy(cols[12]);
if (!cols[13].isEmpty()) {
entity.setAuditedAt(LocalDateTime.parse(cols[13], formatter));
}
entity.setPayloadHash(cols[14]);
entity.setSignValue(cols[15]);
if (!cols[16].isEmpty()) {
entity.setCreateTime(LocalDateTime.parse(cols[16], formatter));
}
list.add(entity);
}
return list;
}
private void recordRestoreAuditLog(String fileName, int total, int skipped, int success, boolean restoreSuccess) {
OperationAuditCommand command = new OperationAuditCommand();
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
//todo 是否考虑为null的情况
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
Object roleObj = request.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE);
command.setOperatorRoleCode(OperatorRoleCode.valueOf(roleObj.toString()));
Object levelObj = request.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL);
command.setOperatorAuthLevel(AuthLevel.valueOf(levelObj.toString()));
command.setRemoteIp(request.getRemoteAddr());
}
command.setModuleCode(ModuleCode.LOG);
command.setActionType(ActionType.EXECUTE);
command.setSummary("执行审计日志恢复");
if (restoreSuccess){
command.setOperationResult(OperationResult.SUCCESS);
} else{
command.setOperationResult(OperationResult.FAILED);
}
operationAuditService.record(command, false);
}
private static byte[] sm3(byte[] data) {
SM3Digest digest = new SM3Digest();
digest.update(data, 0, data.length);
byte[] out = new byte[digest.getDigestSize()];
digest.doFinal(out, 0);
return out;
}
}

View File

@ -14,6 +14,7 @@ import org.springframework.stereotype.Service;
public class BackupConfigService {
private final BackupConfigRepository backupConfigRepository;
private final AuditBackupService auditBackupService;
public BackupConfigResponse getConfig(){
BackupConfigEntity entity = backupConfigRepository.find().orElse(null);
@ -22,6 +23,7 @@ public class BackupConfigService {
response.setId(entity.getId());
response.setEnable(entity.getEnable());
response.setCronExp(entity.getCronExp());
response.setRetentionCount(entity.getRetentionCount());
response.setLastBackupTime(entity.getLastBackupTime());
}
return response;
@ -32,17 +34,16 @@ public class BackupConfigService {
if (!CronExpression.isValidExpression(cronExp)) {
throw new IllegalArgumentException("Cron表达式格式不正确请重新输入");
}
BackupConfigEntity entity = new BackupConfigEntity();
BackupConfigResponse resp = getConfig();
if (resp != null){
resp.setId(entity.getId());
resp.setLastBackupTime(entity.getLastBackupTime());
}
BackupConfigEntity entity = backupConfigRepository.find().orElseGet(BackupConfigEntity::new);
entity.setEnable(req.getEnable());
entity.setCronExp(req.getCronExp());
entity.setRetentionCount(req.getRetentionCount());
backupConfigRepository.saveOrUpdate(entity);
//更新后重启备份服务
auditBackupService.restartBackupTask(entity);
}
private String trim(String str) {

View File

@ -0,0 +1,30 @@
package com.cisd.tms.modules.log.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.cisd.tms.modules.log.dto.BackupRecordPageRequest;
import com.cisd.tms.modules.log.dto.BackupRecordResponse;
import com.cisd.tms.modules.log.entity.BackupRecordEntity;
import com.cisd.tms.modules.log.repository.BackupRecordRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
@Repository
@Service
@RequiredArgsConstructor
public class BackupRecordService {
private final BackupRecordRepository backupRecordRepository;
public IPage<BackupRecordResponse> queryPage(BackupRecordPageRequest req) {
IPage<BackupRecordEntity> entityPage = backupRecordRepository.findPage(req);
return entityPage.convert(entity -> {
BackupRecordResponse resp = new BackupRecordResponse();
resp.setRecordId(entity.getRecordId());
resp.setBackupTime(entity.getCreateTime());
resp.setBackupType(entity.getBackupType());
resp.setBackupResult(entity.getBackupResult());
return resp;
});
}
}

View File

@ -137,9 +137,10 @@ public class OperationAuditService {
resp.setAuditComment(entity.getAuditComment());
resp.setAuditedBy(entity.getAuditedBy());
resp.setRemoteIp(entity.getRemoteIp());
resp.setErrorMessage(entity.getErrorMessage());
resp.setSignValue(entity.getSignValue());
resp.setOccurredAt(entity.getOccurredAt());
resp.setAuditedAt(entity.getAuditedAt());
resp.setSummary(entity.getSummary());
return resp;
});

View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cisd.tms.modules.log.mapper.OperationAuditLogMapper">
<insert id="addBatch">
INSERT INTO tms_operation_audit_log (
id,
log_id,
operator_role_code,
operator_auth_level,
module_code,
action_type,
remote_ip,
operation_result,
summary,
error_message,
audit_status,
audit_result,
audit_comment,
audited_by,
audited_at,
payload_hash,
sign_value,
occurred_at,
create_time
)
VALUES
<foreach collection="list" item="item" separator=",">
(
#{item.id},
#{item.logId},
#{item.operatorRoleCode},
#{item.operatorAuthLevel},
#{item.moduleCode},
#{item.actionType},
#{item.remoteIp},
#{item.operationResult},
#{item.summary},
#{item.errorMessage},
#{item.auditStatus},
#{item.auditResult},
#{item.auditComment},
#{item.auditedBy},
#{item.auditedAt},
#{item.payloadHash},
#{item.signValue},
#{item.occurredAt},
#{item.createTime}
)
</foreach>
</insert>
</mapper>

View File

@ -5,12 +5,19 @@ import org.junit.jupiter.api.Test;
import java.io.File;
public class AuditBackupServiceTest {
private static final String TEMP_RESTORE_DIR = "./tmp/audit_restore/";
@Test
public void FileTest(){
String filePath = "./data/backup/audit_log_" + ".csv";
String filePath = "/data/backup/audit_log_" + ".csv";
File file = new File(filePath);
System.out.print(file.getParentFile());
// Path extractDir = Paths.get(TEMP_RESTORE_DIR, String.valueOf(1));
// System.out.print(extractDir);
}
}