日志备份和恢复
This commit is contained in:
parent
00859fed97
commit
96d706f634
@ -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());
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
|
||||
}
|
||||
@ -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")
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
});
|
||||
|
||||
|
||||
55
src/main/resources/mapper/log/OperationAuditLogMapper.xml
Normal file
55
src/main/resources/mapper/log/OperationAuditLogMapper.xml
Normal 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>
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user