日志记录和审计、 备份配置
This commit is contained in:
parent
1f9b7297a2
commit
ec3ba7c60d
376
docs/plans/2026-04-09-operation-audit-log-design.md
Normal file
376
docs/plans/2026-04-09-operation-audit-log-design.md
Normal file
@ -0,0 +1,376 @@
|
||||
# 管理员操作审计功能设计
|
||||
|
||||
## 1. 目标
|
||||
|
||||
统一使用 `tms_operation_audit_log` 记录所有敏感管理员操作,覆盖:
|
||||
|
||||
- 登录、登出
|
||||
- 增删改
|
||||
- 执行类操作:初始化、重置、升级、回滚、重启、密钥操作
|
||||
- 非法操作:拒绝访问、权限不足、重放、签名失败等
|
||||
|
||||
设计原则:
|
||||
|
||||
- 只保留一张日志表
|
||||
- 敏感操作日志写入失败时,业务直接失败
|
||||
- 日志在落库前完成 `SM3` 摘要与 `SM2` 签名
|
||||
- 人工审计字段不参与首签
|
||||
|
||||
## 2. 表结构
|
||||
|
||||
表名:`tms_operation_audit_log`
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS tms_operation_audit_log (
|
||||
id BIGINT PRIMARY KEY,
|
||||
log_id VARCHAR(64) NOT NULL COMMENT '日志唯一标识',
|
||||
operator_role_code VARCHAR(64) NOT NULL COMMENT '操作角色编码',
|
||||
operator_auth_level VARCHAR(32) NULL COMMENT '操作时认证等级',
|
||||
module_code VARCHAR(32) NOT NULL COMMENT '业务模块',
|
||||
action_type VARCHAR(32) NOT NULL COMMENT '操作动作',
|
||||
remote_ip VARCHAR(64) NULL COMMENT '来源IP',
|
||||
operation_result VARCHAR(16) NOT NULL COMMENT '操作结果: SUCCESS/FAILED/DENIED',
|
||||
summary VARCHAR(512) NOT NULL COMMENT '操作摘要',
|
||||
error_message VARCHAR(1024) NULL COMMENT '失败原因',
|
||||
audit_status VARCHAR(16) NOT NULL COMMENT '审计状态: PENDING/REVIEWED',
|
||||
audit_result VARCHAR(16) NULL COMMENT '审计结果: PASS/REJECT/NEED_VERIFY',
|
||||
audit_comment VARCHAR(1024) NULL COMMENT '审计意见',
|
||||
audited_by VARCHAR(64) NULL COMMENT '审计人',
|
||||
audited_at DATETIME(3) NULL COMMENT '审计时间',
|
||||
payload_hash VARCHAR(128) NOT NULL COMMENT 'SM3摘要',
|
||||
sign_value TEXT NOT NULL COMMENT 'SM2签名值',
|
||||
occurred_at DATETIME(3) NOT NULL COMMENT '操作发生时间',
|
||||
create_time DATETIME(3) NOT NULL COMMENT '落库时间',
|
||||
UNIQUE KEY uk_tms_operation_audit_log_log_id (log_id),
|
||||
KEY idx_tms_operation_audit_log_role_code (operator_role_code),
|
||||
KEY idx_tms_operation_audit_log_module_code (module_code),
|
||||
KEY idx_tms_operation_audit_log_action_type (action_type),
|
||||
KEY idx_tms_operation_audit_log_result (operation_result),
|
||||
KEY idx_tms_operation_audit_log_audit_status (audit_status),
|
||||
KEY idx_tms_operation_audit_log_occurred_at (occurred_at)
|
||||
);
|
||||
```
|
||||
|
||||
## 3. 枚举设计
|
||||
|
||||
### 3.1 `module_code`
|
||||
|
||||
- `AUTH`
|
||||
- `INIT`
|
||||
- `UPGRADE`
|
||||
- `DEVICE`
|
||||
- `NETWORK`
|
||||
- `KEY`
|
||||
- `SYSTEM`
|
||||
- `SECURITY`
|
||||
|
||||
### 3.2 `action_type`
|
||||
|
||||
- `LOGIN`
|
||||
- `LOGOUT`
|
||||
- `CREATE`
|
||||
- `UPDATE`
|
||||
- `DELETE`
|
||||
- `EXECUTE`
|
||||
- `RESET`
|
||||
- `ENABLE`
|
||||
- `DISABLE`
|
||||
- `BIND`
|
||||
- `IMPORT`
|
||||
- `EXPORT`
|
||||
- `BACKUP`
|
||||
- `RECOVER`
|
||||
- `RESTART`
|
||||
- `ACCESS`
|
||||
|
||||
### 3.3 `operation_result`
|
||||
|
||||
- `SUCCESS`
|
||||
- `FAILED`
|
||||
- `DENIED`
|
||||
|
||||
### 3.4 `audit_status`
|
||||
|
||||
- `PENDING`
|
||||
- `REVIEWED`
|
||||
|
||||
### 3.5 `audit_result`
|
||||
|
||||
- `PASS`
|
||||
- `REJECT`
|
||||
- `NEED_VERIFY`
|
||||
|
||||
## 4. 摘要与签名
|
||||
|
||||
### 4.1 首签字段
|
||||
|
||||
入库前参与 `payload_hash` 和 `sign_value` 计算的字段:
|
||||
|
||||
- `log_id`
|
||||
- `operator_role_code`
|
||||
- `operator_auth_level`
|
||||
- `module_code`
|
||||
- `action_type`
|
||||
- `remote_ip`
|
||||
- `operation_result`
|
||||
- `summary`
|
||||
- `occurred_at`
|
||||
|
||||
### 4.2 不参与首签的字段
|
||||
|
||||
以下字段属于后续审计动作或补充信息,不参与首签:
|
||||
|
||||
- `audit_status`
|
||||
- `audit_result`
|
||||
- `audit_comment`
|
||||
- `audited_by`
|
||||
- `audited_at`
|
||||
- `error_message`
|
||||
|
||||
### 4.3 规范化串
|
||||
|
||||
建议按固定字段顺序拼接:
|
||||
|
||||
```text
|
||||
log_id=...
|
||||
operator_role_code=...
|
||||
operator_auth_level=...
|
||||
module_code=...
|
||||
action_type=...
|
||||
remote_ip=...
|
||||
operation_result=...
|
||||
summary=...
|
||||
occurred_at=...
|
||||
```
|
||||
|
||||
规范要求:
|
||||
|
||||
- 字段顺序固定
|
||||
- `null` 统一为空串
|
||||
- 首尾空格统一裁剪
|
||||
- 时间统一使用 UTC 与固定格式
|
||||
|
||||
### 4.4 签名流程
|
||||
|
||||
1. 构造 `canonical_payload`
|
||||
2. 计算 `payload_hash = SM3(canonical_payload)`
|
||||
3. 计算 `sign_value = SM2(privateKey, canonical_payload)`
|
||||
4. 一次性写入数据库
|
||||
|
||||
## 5. Java 设计草案
|
||||
|
||||
核心对象:
|
||||
|
||||
- `OperationAuditLogEntity`
|
||||
- `OperationAuditLogRepository`
|
||||
- `OperationAuditService`
|
||||
- `OperationAuditCommand`
|
||||
- `OperationAuditSigner`
|
||||
|
||||
服务接口建议:
|
||||
|
||||
```java
|
||||
public interface OperationAuditService {
|
||||
|
||||
void record(OperationAuditCommand command);
|
||||
|
||||
void review(String logId, AuditResult auditResult, String auditComment, String auditedBy);
|
||||
}
|
||||
```
|
||||
|
||||
`OperationAuditCommand` 建议字段:
|
||||
|
||||
- `operatorRoleCode`
|
||||
- `operatorAuthLevel`
|
||||
- `moduleCode`
|
||||
- `actionType`
|
||||
- `remoteIp`
|
||||
- `operationResult`
|
||||
- `summary`
|
||||
- `errorMessage`
|
||||
|
||||
## 6. 日志记录流程
|
||||
|
||||
统一入口:`OperationAuditService.record(...)`
|
||||
|
||||
处理流程:
|
||||
|
||||
1. 业务侧提交 `OperationAuditCommand`
|
||||
2. 服务补齐:
|
||||
- `log_id`
|
||||
- `occurred_at`
|
||||
- `audit_status = PENDING`
|
||||
3. 生成 `payload_hash` 与 `sign_value`
|
||||
4. 插入 `tms_operation_audit_log`
|
||||
5. 若插入失败:
|
||||
- 对敏感操作直接抛异常,业务失败
|
||||
|
||||
## 7. 注解与 AOP 接入方案
|
||||
|
||||
### 7.1 注解定义
|
||||
|
||||
```java
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface AuditedOperation {
|
||||
|
||||
String module();
|
||||
|
||||
String action();
|
||||
|
||||
String summary() default "";
|
||||
|
||||
boolean sensitive() default true;
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 AOP 职责
|
||||
|
||||
切面负责:
|
||||
|
||||
- 读取注解元数据
|
||||
- 从请求上下文提取:
|
||||
- 当前角色
|
||||
- 当前认证等级
|
||||
- 来源 IP
|
||||
- 环绕执行目标方法
|
||||
- 成功时记录 `SUCCESS`
|
||||
- 捕获业务异常时记录 `FAILED`
|
||||
- 捕获权限或安全拒绝时记录 `DENIED`
|
||||
- 对 `sensitive = true` 的操作,日志写入失败时重新抛错
|
||||
|
||||
### 7.3 设计原则
|
||||
|
||||
- AOP 负责公共字段、结果判定和统一落库
|
||||
- 业务方法负责提供准确的 `summary`
|
||||
- 不在 AOP 中自动抓取完整请求参数,避免日志带入密码、令牌、密钥材料等敏感数据
|
||||
|
||||
## 8. 接口清单
|
||||
|
||||
### 8.1 首批需要接入审计的接口
|
||||
|
||||
#### 认证与角色管理
|
||||
|
||||
- `POST /api/v1/auth/password-login`
|
||||
- `POST /api/v1/auth/ukey-login`
|
||||
- `POST /api/v1/auth/logout`
|
||||
- `POST /api/v1/auth/change-password`
|
||||
- `POST /api/v1/auth/roles/{roleCode}/enable`
|
||||
- `POST /api/v1/auth/roles/{roleCode}/reset-password`
|
||||
- `POST /api/v1/auth/roles/{roleCode}/ukeys/bind`
|
||||
|
||||
#### 初始化与重置
|
||||
|
||||
- `POST /api/v1/init/tasks`
|
||||
- `POST /api/v1/init/tasks/{taskId}/execute`
|
||||
- `POST /api/v1/init/reset/tasks`
|
||||
- `POST /api/v1/init/reset/tasks/{taskId}/execute`
|
||||
|
||||
#### 升级管理
|
||||
|
||||
- `POST /api/v1/upgrades`
|
||||
- `POST /api/v1/upgrades/{taskId}/execute`
|
||||
- `POST /api/v1/upgrades/{taskId}/rollback`
|
||||
|
||||
#### 设备与网络
|
||||
|
||||
- `POST /api/v1/device/restart`
|
||||
- 网络配置写接口
|
||||
- IP 白名单增删改接口
|
||||
|
||||
#### 密钥与密码卡
|
||||
|
||||
- LMK / IK / UKey / 密钥导入导出、备份恢复、销毁相关写接口
|
||||
|
||||
#### 安全事件
|
||||
|
||||
- 认证失败
|
||||
- 权限不足
|
||||
- 重放拦截
|
||||
- 签名校验失败
|
||||
|
||||
### 8.2 日志分页查询接口
|
||||
|
||||
- `GET /api/v1/audit-logs`
|
||||
|
||||
查询条件:
|
||||
|
||||
- `operatorRoleCode`
|
||||
- `moduleCode`
|
||||
- `actionType`
|
||||
- `operationResult`
|
||||
- `auditStatus`
|
||||
- `auditResult`
|
||||
- `remoteIp`
|
||||
- `dateFrom`
|
||||
- `dateTo`
|
||||
- `keyword`
|
||||
|
||||
默认排序:
|
||||
|
||||
- `occurred_at DESC`
|
||||
|
||||
### 8.3 日志详情接口
|
||||
|
||||
- `GET /api/v1/audit-logs/{logId}`
|
||||
|
||||
详情建议返回:
|
||||
|
||||
- 基础日志字段
|
||||
- 审计字段
|
||||
- `payload_hash`
|
||||
- `sign_value`
|
||||
|
||||
### 8.4 日志审计接口
|
||||
|
||||
- `POST /api/v1/audit-logs/{logId}/review`
|
||||
|
||||
请求示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"auditResult": "PASS",
|
||||
"auditComment": "复核通过"
|
||||
}
|
||||
```
|
||||
|
||||
更新规则:
|
||||
|
||||
- 仅审计管理员可操作
|
||||
- 仅允许 `PENDING -> REVIEWED`
|
||||
- 更新字段:
|
||||
- `audit_status`
|
||||
- `audit_result`
|
||||
- `audit_comment`
|
||||
- `audited_by`
|
||||
- `audited_at`
|
||||
|
||||
## 9. 失败策略
|
||||
|
||||
敏感操作统一采用失败关闭:
|
||||
|
||||
- 审计日志写入失败,业务直接失败
|
||||
|
||||
敏感操作范围包括:
|
||||
|
||||
- 登录与登出
|
||||
- 角色管理
|
||||
- 口令变更
|
||||
- UKey 管理
|
||||
- 初始化与重置
|
||||
- 升级与回滚
|
||||
- 网络配置
|
||||
- 白名单变更
|
||||
- 密钥与密码卡操作
|
||||
- 安全拦截事件
|
||||
|
||||
## 10. 推荐实现顺序
|
||||
|
||||
1. Flyway 建表,删除旧 `tms_auth_audit_log` / `tms_security_event`
|
||||
2. 增加枚举、Entity、Repository
|
||||
3. 增加 `OperationAuditSigner`(SM3 / SM2)
|
||||
4. 增加 `OperationAuditService`
|
||||
5. 增加 `@AuditedOperation` 与 AOP
|
||||
6. 增加分页查询与详情接口
|
||||
7. 增加审计接口
|
||||
8. 在首批敏感接口上补齐注解与摘要
|
||||
@ -6,6 +6,9 @@ import com.cisd.tms.modules.auth.enums.AuthLevel;
|
||||
import com.cisd.tms.modules.auth.enums.RoleCode;
|
||||
import com.cisd.tms.modules.auth.security.RequireInternalAuth;
|
||||
import com.cisd.tms.modules.auth.service.AuthAdminService;
|
||||
import com.cisd.tms.modules.log.annotation.AuditedOperation;
|
||||
import com.cisd.tms.modules.log.enums.ActionType;
|
||||
import com.cisd.tms.modules.log.enums.ModuleCode;
|
||||
import com.cisd.tms.modules.mk.dto.UKeySignDTO;
|
||||
import com.cisd.tms.modules.mk.dto.UKeySignResult;
|
||||
import com.cisd.tms.security.internal.InternalApiAuthInterceptor;
|
||||
@ -14,13 +17,8 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/auth")
|
||||
@ -34,6 +32,7 @@ public class AuthAdminController {
|
||||
@PostMapping("/roles/{roleCode}/enable")
|
||||
@Operation(summary = "启用角色", description = "仅允许 KEY_ADMIN FULL 会话启用目标角色。")
|
||||
@RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL)
|
||||
@AuditedOperation(module = ModuleCode.AUTH, action = ActionType.ENABLE, summary = "启用角色")
|
||||
public ApiResponse<Void> enableRole(@PathVariable("roleCode") String roleCode, HttpServletRequest request) {
|
||||
authAdminService.enableRole(
|
||||
(String) request.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE),
|
||||
@ -46,6 +45,7 @@ public class AuthAdminController {
|
||||
@PostMapping("/roles/{roleCode}/reset-password")
|
||||
@Operation(summary = "重置角色密码", description = "仅允许 KEY_ADMIN FULL 会话重置目标角色密码。")
|
||||
@RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL)
|
||||
@AuditedOperation(module = ModuleCode.AUTH, action = ActionType.RESET, summary = "重置角色密码")
|
||||
public ApiResponse<Void> resetPassword(@PathVariable("roleCode") String roleCode, HttpServletRequest request) {
|
||||
authAdminService.resetPassword(
|
||||
(String) request.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE),
|
||||
@ -58,6 +58,7 @@ public class AuthAdminController {
|
||||
@PostMapping("/roles/{roleCode}/ukeys/bind")
|
||||
@Operation(summary = "绑定角色 UKey", description = "仅允许 KEY_ADMIN FULL 会话登记目标角色的 UKey 绑定信息。")
|
||||
@RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL)
|
||||
@AuditedOperation(module = ModuleCode.AUTH, action = ActionType.BIND, summary = "绑定角色 UKey")
|
||||
public ApiResponse<Void> bindUkey(
|
||||
@PathVariable("roleCode") String roleCode,
|
||||
@Valid @RequestBody UkeyBindRequest request,
|
||||
|
||||
@ -1,28 +1,19 @@
|
||||
package com.cisd.tms.modules.auth.controller;
|
||||
|
||||
import com.cisd.tms.common.api.ApiResponse;
|
||||
import com.cisd.tms.modules.auth.dto.CaptchaResponse;
|
||||
import com.cisd.tms.modules.auth.dto.ChangePasswordRequest;
|
||||
import com.cisd.tms.modules.auth.dto.CurrentUserResponse;
|
||||
import com.cisd.tms.modules.auth.dto.LoginResponse;
|
||||
import com.cisd.tms.modules.auth.dto.PasswordLoginRequest;
|
||||
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomRequest;
|
||||
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse;
|
||||
import com.cisd.tms.modules.auth.dto.UkeyLoginRequest;
|
||||
import com.cisd.tms.modules.auth.dto.*;
|
||||
import com.cisd.tms.modules.auth.service.AuthService;
|
||||
import com.cisd.tms.modules.log.annotation.AuditedOperation;
|
||||
import com.cisd.tms.modules.log.enums.ActionType;
|
||||
import com.cisd.tms.modules.log.enums.ModuleCode;
|
||||
import com.cisd.tms.security.internal.InternalApiAuthInterceptor;
|
||||
import com.cisd.tms.security.internal.ReplayProtected;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/auth")
|
||||
@ -34,6 +25,7 @@ public class AuthController {
|
||||
|
||||
@PostMapping("/password-login")
|
||||
@Operation(summary = "口令登录", description = "按旧系统受限登录流程签发 LIMITED 会话。")
|
||||
@AuditedOperation(module = ModuleCode.AUTH, action = ActionType.LOGIN, summary = "口令登录")
|
||||
public ApiResponse<LoginResponse> passwordLogin(@Valid @RequestBody PasswordLoginRequest request) {
|
||||
return ApiResponse.success(authService.passwordLogin(request));
|
||||
}
|
||||
@ -46,6 +38,7 @@ public class AuthController {
|
||||
|
||||
@PostMapping("/ukey-login")
|
||||
@Operation(summary = "UKey 登录", description = "按旧系统标准 UKey 校验顺序签发 FULL 会话。")
|
||||
@AuditedOperation(module = ModuleCode.AUTH, action = ActionType.LOGIN, summary = "UKey 登录")
|
||||
public ApiResponse<LoginResponse> ukeyLogin(@Valid @RequestBody UkeyLoginRequest request) {
|
||||
return ApiResponse.success(authService.ukeyLogin(request));
|
||||
}
|
||||
@ -66,6 +59,7 @@ public class AuthController {
|
||||
|
||||
@PostMapping("/logout")
|
||||
@Operation(summary = "退出当前会话", description = "使当前会话失效。")
|
||||
@AuditedOperation(module = ModuleCode.AUTH, action = ActionType.LOGOUT, summary = "退出当前会话")
|
||||
public ApiResponse<Void> logout(HttpServletRequest request) {
|
||||
authService.logout((String) request.getAttribute(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN));
|
||||
return ApiResponse.success();
|
||||
@ -73,6 +67,7 @@ public class AuthController {
|
||||
|
||||
@PostMapping("/change-password")
|
||||
@Operation(summary = "修改当前角色口令", description = "基于当前会话校验并更新当前角色口令。")
|
||||
@AuditedOperation(module = ModuleCode.AUTH, action = ActionType.UPDATE, summary = "修改当前角色口令")
|
||||
@ReplayProtected
|
||||
public ApiResponse<Void> changePassword(@Valid @RequestBody ChangePasswordRequest request, HttpServletRequest httpRequest) {
|
||||
authService.changePassword(
|
||||
|
||||
@ -3,23 +3,27 @@ package com.cisd.tms.modules.auth.security;
|
||||
import com.cisd.tms.common.api.ApiResponse;
|
||||
import com.cisd.tms.common.enums.ErrorCode;
|
||||
import com.cisd.tms.common.util.HttpResponseUtil;
|
||||
import com.cisd.tms.modules.log.service.OperationAuditService;
|
||||
import com.cisd.tms.security.internal.InternalApiAuthInterceptor;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Component
|
||||
public class InternalAuthorizationInterceptor implements HandlerInterceptor {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
private final OperationAuditService operationAuditService;
|
||||
|
||||
public InternalAuthorizationInterceptor(ObjectMapper objectMapper) {
|
||||
public InternalAuthorizationInterceptor(ObjectMapper objectMapper, OperationAuditService operationAuditService) {
|
||||
this.objectMapper = objectMapper;
|
||||
this.operationAuditService = operationAuditService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -33,11 +37,13 @@ public class InternalAuthorizationInterceptor implements HandlerInterceptor {
|
||||
String currentRole = (String) request.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE);
|
||||
if (!requireInternalAuth.role().getCode().equals(currentRole)) {
|
||||
writeForbidden(response, "role not allowed");
|
||||
operationAuditService.recordDeniedLog(request, handlerMethod, "role not allowed");
|
||||
return false;
|
||||
}
|
||||
String currentAuthLevel = (String) request.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL);
|
||||
if (!requireInternalAuth.authLevel().name().equals(currentAuthLevel)) {
|
||||
writeForbidden(response, "auth level not allowed");
|
||||
operationAuditService.recordDeniedLog(request, handlerMethod, "auth level not allowed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,10 +8,12 @@ import com.cisd.tms.modules.device.dto.DeviceRuntimeStatusResponse;
|
||||
import com.cisd.tms.modules.device.service.DeviceProfileService;
|
||||
import com.cisd.tms.modules.device.service.DeviceRuntimeStatusService;
|
||||
import com.cisd.tms.modules.device.service.DeviceService;
|
||||
import com.cisd.tms.modules.log.annotation.AuditedOperation;
|
||||
import com.cisd.tms.modules.log.enums.ActionType;
|
||||
import com.cisd.tms.modules.log.enums.ModuleCode;
|
||||
import com.cisd.tms.security.internal.InternalApiAuthInterceptor;
|
||||
import com.cisd.tms.security.internal.ReplayProtected;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@ -57,6 +59,7 @@ public class DeviceController {
|
||||
@PostMapping("/restart")
|
||||
@Operation(summary = "重启设备服务", description = "受理设备重启请求,并记录触发操作的内部用户。")
|
||||
@ReplayProtected
|
||||
@AuditedOperation(module = ModuleCode.DEVICE, action = ActionType.RESTART, summary = "重启设备服务")
|
||||
public ApiResponse<DeviceActionResponse> restart(HttpServletRequest request) {
|
||||
String operator = (String) request.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE);
|
||||
return ApiResponse.success(deviceService.restart(operator));
|
||||
|
||||
@ -2,11 +2,15 @@ package com.cisd.tms.modules.device.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.cisd.tms.common.api.ApiResponse;
|
||||
import com.cisd.tms.modules.device.dto.network.IpWhitelistRequest;
|
||||
import com.cisd.tms.modules.device.dto.network.IpWhitelistResponse;
|
||||
import com.cisd.tms.modules.device.service.IpWhitelistService;
|
||||
import com.cisd.tms.modules.device.dto.network.IpWhitelistRequest;
|
||||
import com.cisd.tms.security.internal.ReplayProtected;
|
||||
import com.cisd.tms.modules.log.annotation.AuditedOperation;
|
||||
import com.cisd.tms.modules.log.enums.ActionType;
|
||||
import com.cisd.tms.modules.log.enums.ModuleCode;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@ -22,7 +26,7 @@ public class IpWhitelistController {
|
||||
|
||||
@PostMapping("/add")
|
||||
@Operation(summary = "添加IP白名单", description = "新增一条IP白名单记录")
|
||||
@ReplayProtected
|
||||
// @ReplayProtected
|
||||
public ApiResponse<Void> addWhitelist(@RequestBody IpWhitelistRequest req) {
|
||||
ipWhitelistService.addWhitelist(req);
|
||||
return ApiResponse.success();
|
||||
@ -31,7 +35,8 @@ public class IpWhitelistController {
|
||||
|
||||
@PostMapping("/update")
|
||||
@Operation(summary = "更新IP白名单", description = "根据ID更新IP白名单信息")
|
||||
@ReplayProtected
|
||||
// @ReplayProtected
|
||||
@AuditedOperation(module = ModuleCode.DEVICE, action = ActionType.UPDATE, summary = "更新IP白名单")
|
||||
public ApiResponse<Void> updateWhitelist(@RequestBody IpWhitelistRequest req) {
|
||||
ipWhitelistService.updateWhitelist(req);
|
||||
return ApiResponse.success();
|
||||
@ -39,7 +44,8 @@ public class IpWhitelistController {
|
||||
|
||||
@PostMapping("/delete/{id}")
|
||||
@Operation(summary = "删除IP白名单", description = "根据ID删除指定IP白名单")
|
||||
@ReplayProtected
|
||||
// @ReplayProtected
|
||||
@AuditedOperation(module = ModuleCode.DEVICE, action = ActionType.DELETE, summary = "删除IP白名单")
|
||||
public ApiResponse<Void> deleteWhitelist(@PathVariable Long id) {
|
||||
ipWhitelistService.deleteWhitelist(id);
|
||||
return ApiResponse.success();
|
||||
@ -49,8 +55,12 @@ public class IpWhitelistController {
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "分页查询IP白名单", description = "支持分页查询IP白名单列表")
|
||||
public ApiResponse<IPage<IpWhitelistResponse>> getWhitelistPage(@RequestParam int pageNum,
|
||||
@RequestParam int pageSize) {
|
||||
IPage<IpWhitelistResponse> page = ipWhitelistService.getWhitelistPage(pageNum, pageSize);
|
||||
@RequestParam int pageSize,
|
||||
@Parameter(
|
||||
description = "地址类型",
|
||||
schema = @Schema(allowableValues = {"HOST", "SUBNET"}, example = "HOST")
|
||||
)String addressType) {
|
||||
IPage<IpWhitelistResponse> page = ipWhitelistService.getWhitelistPage(pageNum, pageSize, addressType);
|
||||
return ApiResponse.success(page);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,9 @@ package com.cisd.tms.modules.device.controller;
|
||||
import com.cisd.tms.common.api.ApiResponse;
|
||||
import com.cisd.tms.modules.device.dto.network.*;
|
||||
import com.cisd.tms.modules.device.service.NetworkConfigService;
|
||||
import com.cisd.tms.modules.log.annotation.AuditedOperation;
|
||||
import com.cisd.tms.modules.log.enums.ActionType;
|
||||
import com.cisd.tms.modules.log.enums.ModuleCode;
|
||||
import com.cisd.tms.security.internal.ReplayProtected;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@ -22,22 +25,33 @@ public class NetworkConfigController {
|
||||
this.networkConfigService = networkConfigService;
|
||||
}
|
||||
|
||||
|
||||
@AuditedOperation(
|
||||
module = ModuleCode.NETWORK,
|
||||
action = ActionType.CREATE,
|
||||
summary = "获取网络信息"
|
||||
)
|
||||
@Operation(summary = "获取网络信息", description = "获取当前设备的所有网络连接信息")
|
||||
@GetMapping("/network-info")
|
||||
public ApiResponse<List<NetworkInfoResponse>> getNetworkInfo(){
|
||||
return ApiResponse.success(networkConfigService.getNetworkInfo());
|
||||
}
|
||||
|
||||
@AuditedOperation(
|
||||
module = ModuleCode.NETWORK,
|
||||
action = ActionType.DELETE,
|
||||
summary = "获取IPv4配置信息"
|
||||
)
|
||||
@Operation(summary = "获取IPv4配置信息", description = "根据设备名称获取指定网络接口的IPv4配置详情")
|
||||
@GetMapping("/ipv4-info/{deviceName}")
|
||||
public ApiResponse<Ipv4InfoResponse> getIpv4Info(@PathVariable String deviceName){
|
||||
return ApiResponse.success(networkConfigService.getIpv4Info(deviceName));
|
||||
}
|
||||
|
||||
|
||||
@Operation(summary = "设置IPv4配置", description = "为指定网络接口配置IPv4地址、掩码、网关等信息")
|
||||
@PostMapping("/ipv4-config/set")
|
||||
@ReplayProtected
|
||||
@AuditedOperation(module = ModuleCode.NETWORK, action = ActionType.CREATE, summary = "设置IPv4配置")
|
||||
public ApiResponse<Void> setIpv4Config(@RequestBody Ipv4ConfigRequest req) {
|
||||
networkConfigService.setIpv4Config(req);
|
||||
return ApiResponse.success();
|
||||
@ -53,6 +67,7 @@ public class NetworkConfigController {
|
||||
@Operation(summary = "设置IPv6配置", description = "为指定网络接口配置IPv6地址、前缀长度、网关等信息")
|
||||
@PostMapping("/ipv6-config/set")
|
||||
@ReplayProtected
|
||||
@AuditedOperation(module = ModuleCode.NETWORK, action = ActionType.CREATE, summary = "设置IPv6配置")
|
||||
public ApiResponse<Void> setIpv6Config(@RequestBody Ipv6ConfigRequest req) {
|
||||
networkConfigService.setIpv6Config(req);
|
||||
return ApiResponse.success();
|
||||
@ -67,6 +82,7 @@ public class NetworkConfigController {
|
||||
@Operation(summary = "创建Bond", description = "创建一个新的Bond聚合接口,需指定名称、模式和从属接口")
|
||||
@PostMapping("/bond/create")
|
||||
@ReplayProtected
|
||||
@AuditedOperation(module = ModuleCode.NETWORK, action = ActionType.CREATE, summary = "创建Bond")
|
||||
public ApiResponse<Void> createBond(@RequestBody BondCreateRequest req) {
|
||||
networkConfigService.createBond(req);
|
||||
return ApiResponse.success();
|
||||
@ -76,6 +92,7 @@ public class NetworkConfigController {
|
||||
@Operation(summary = "删除Bond", description = "根据Bond名称删除指定的聚合接口")
|
||||
@PostMapping("/bond/delete/{bondName}")
|
||||
@ReplayProtected
|
||||
@AuditedOperation(module = ModuleCode.NETWORK, action = ActionType.DELETE, summary = "删除Bond")
|
||||
public ApiResponse<Void> deleteBond(@PathVariable("bondName") String bondName){
|
||||
networkConfigService.deleteBond(bondName);
|
||||
return ApiResponse.success();
|
||||
@ -84,6 +101,7 @@ public class NetworkConfigController {
|
||||
@Operation(summary = "添加从属接口到Bond", description = "向指定Bond中添加一个或多个从属网络接口")
|
||||
@PostMapping("/bond-slave/add")
|
||||
@ReplayProtected
|
||||
@AuditedOperation(module = ModuleCode.NETWORK, action = ActionType.UPDATE, summary = "添加从属接口到Bond")
|
||||
public ApiResponse<Void> addSlavesTOBond(@RequestBody BondAddSlavesRequest req) {
|
||||
networkConfigService.addSlavesTOBond(req);
|
||||
return ApiResponse.success();
|
||||
@ -92,6 +110,7 @@ public class NetworkConfigController {
|
||||
@Operation(summary = "从Bond中移除从属接口", description = "从指定Bond中移除一个或多个从属网络接口")
|
||||
@PostMapping("/bond-slave/remove")
|
||||
@ReplayProtected
|
||||
@AuditedOperation(module = ModuleCode.NETWORK, action = ActionType.DELETE, summary = "从Bond中移除从属接口")
|
||||
public ApiResponse<String> removeSlaveFromBond(@RequestBody BondRemoveSlaveRequest req) {
|
||||
String resultMessage = networkConfigService.removeSlaveFromBond(req);
|
||||
return ApiResponse.success(resultMessage);
|
||||
@ -120,6 +139,7 @@ public class NetworkConfigController {
|
||||
@Operation(summary = "设置Bond模式", description = "修改指定Bond的绑定模式")
|
||||
@PostMapping("/bond/mode/set")
|
||||
@ReplayProtected
|
||||
@AuditedOperation(module = ModuleCode.NETWORK, action = ActionType.UPDATE, summary = "设置Bond模式")
|
||||
public ApiResponse<Void> setBondMode(@RequestBody BondModifyModeRequest req) {
|
||||
networkConfigService.setBondMode(req);
|
||||
return ApiResponse.success();
|
||||
@ -135,6 +155,7 @@ public class NetworkConfigController {
|
||||
@Operation(summary = "设置默认路由", description = "配置或修改系统的默认网关路由")
|
||||
@PostMapping("/routes/default/set")
|
||||
@ReplayProtected
|
||||
@AuditedOperation(module = ModuleCode.NETWORK, action = ActionType.UPDATE, summary = "设置默认路由")
|
||||
public ApiResponse<Void> setDefaultRoute(@RequestBody SetDefaultRouteRequest req) {
|
||||
networkConfigService.setDefaultRoute(req);
|
||||
return ApiResponse.success();
|
||||
@ -143,6 +164,7 @@ public class NetworkConfigController {
|
||||
@Operation(summary = "添加静态路由", description = "新增一条静态路由规则")
|
||||
@PostMapping("/routes/static/add")
|
||||
@ReplayProtected
|
||||
@AuditedOperation(module = ModuleCode.NETWORK, action = ActionType.UPDATE, summary = "添加静态路由")
|
||||
public ApiResponse<Void> addStaticRoute(@RequestBody AddStaticRouteRequest req) {
|
||||
networkConfigService.addStaticRoute(req);
|
||||
return ApiResponse.success();
|
||||
@ -151,6 +173,7 @@ public class NetworkConfigController {
|
||||
@Operation(summary = "删除默认路由", description = "删除指定的默认路由")
|
||||
@PostMapping("/routes/default/delete")
|
||||
@ReplayProtected
|
||||
@AuditedOperation(module = ModuleCode.NETWORK, action = ActionType.DELETE, summary = "删除默认路由")
|
||||
public ApiResponse<Void> deleteDefaultRoute(@RequestBody DeleteDefaultRouteRequest req) {
|
||||
networkConfigService.deleteDefaultRoute(req);
|
||||
return ApiResponse.success();
|
||||
@ -159,6 +182,7 @@ public class NetworkConfigController {
|
||||
@Operation(summary = "删除静态路由", description = "删除指定的静态路由")
|
||||
@PostMapping("/routes/static/delete")
|
||||
@ReplayProtected
|
||||
@AuditedOperation(module = ModuleCode.NETWORK, action = ActionType.DELETE, summary = "删除静态路由")
|
||||
public ApiResponse<Void> deleteStaticRoute(@RequestBody DeleteStaticRouteRequest req) {
|
||||
networkConfigService.deleteStaticRoute(req);
|
||||
return ApiResponse.success();
|
||||
|
||||
@ -12,4 +12,6 @@ public class IpWhitelistResponse {
|
||||
private String ip;
|
||||
@Schema(description = "掩码长度", example = "24")
|
||||
private String mask;
|
||||
@Schema(description = "地址类型", example = "HOST")
|
||||
private String addressType;
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package com.cisd.tms.modules.device.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.cisd.tms.modules.device.dto.network.IpWhitelistRequest;
|
||||
import com.cisd.tms.modules.device.entity.IpWhitelistEntity;
|
||||
|
||||
import java.util.List;
|
||||
@ -22,7 +21,7 @@ public interface IpWhitelistRepository {
|
||||
/**
|
||||
* 分页查询白名单
|
||||
*/
|
||||
Page<IpWhitelistEntity> selectPage(Page<IpWhitelistEntity> page);
|
||||
Page<IpWhitelistEntity> selectPage(int pageNum, int pageSize, String addressType);
|
||||
|
||||
/**
|
||||
* 查询所有白名单
|
||||
|
||||
@ -2,7 +2,6 @@ package com.cisd.tms.modules.device.repository.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.cisd.tms.modules.device.dto.network.IpWhitelistRequest;
|
||||
import com.cisd.tms.modules.device.entity.IpWhitelistEntity;
|
||||
import com.cisd.tms.modules.device.mapper.IpWhitelistMapper;
|
||||
import com.cisd.tms.modules.device.repository.IpWhitelistRepository;
|
||||
@ -53,7 +52,7 @@ public class IpWhiltelistRepositoryImpl implements IpWhitelistRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<IpWhitelistEntity> selectPage(Page<IpWhitelistEntity> page) {
|
||||
public Page<IpWhitelistEntity> selectPage(int pageNum, int pageSize, String addressType) {
|
||||
// LambdaQueryWrapper<IpWhitelistEntity> wrapper = new LambdaQueryWrapper<>();
|
||||
//
|
||||
// if (StringUtils.hasText(req.getIp())) {
|
||||
@ -63,8 +62,15 @@ public class IpWhiltelistRepositoryImpl implements IpWhitelistRepository {
|
||||
// wrapper.eq(IpWhitelistEntity::getMask, req.getMask());
|
||||
// }
|
||||
// wrapper.orderByDesc(IpWhitelistEntity::getCreateTime);
|
||||
Page<IpWhitelistEntity> page = new Page<>(pageNum, pageSize);
|
||||
LambdaQueryWrapper<IpWhitelistEntity> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
return ipWhitelistMapper.selectPage(page, null);
|
||||
if ("SUBNET".equals(addressType)) {
|
||||
wrapper.ne(IpWhitelistEntity::getMask, "32");
|
||||
} else if ("HOST".equals(addressType)){
|
||||
wrapper.eq(IpWhitelistEntity::getMask, "32");
|
||||
}
|
||||
return ipWhitelistMapper.selectPage(page, wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -76,16 +76,19 @@ public class IpWhitelistService {
|
||||
}
|
||||
|
||||
|
||||
public IPage<IpWhitelistResponse> getWhitelistPage(int pageNum, int pageSize) {
|
||||
Page<IpWhitelistEntity> page = new Page<>(pageNum, pageSize);
|
||||
ipWhitelistRepository.selectPage(page);
|
||||
public IPage<IpWhitelistResponse> getWhitelistPage(int pageNum, int pageSize, String addressType) {
|
||||
Page<IpWhitelistEntity> entityPage = ipWhitelistRepository.selectPage(pageNum, pageSize, addressType);
|
||||
|
||||
|
||||
return page.convert(entity -> {
|
||||
return entityPage.convert(entity -> {
|
||||
IpWhitelistResponse response = new IpWhitelistResponse();
|
||||
response.setId(String.valueOf(entity.getId()));
|
||||
response.setIp(entity.getIp());
|
||||
response.setMask(entity.getMask());
|
||||
if ("32".equals(entity.getMask())) {
|
||||
response.setAddressType("HOST");
|
||||
} else {
|
||||
response.setAddressType("SUBNET");
|
||||
}
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,31 +1,19 @@
|
||||
package com.cisd.tms.modules.init.controller;
|
||||
|
||||
import com.cisd.tms.common.api.ApiResponse;
|
||||
import com.cisd.tms.modules.init.dto.InitCreateTaskResponse;
|
||||
import com.cisd.tms.modules.init.dto.CurrentInitConfigResponse;
|
||||
import com.cisd.tms.modules.init.dto.InitPlanTemplateResponse;
|
||||
import com.cisd.tms.modules.init.dto.InitPreviewRequest;
|
||||
import com.cisd.tms.modules.init.dto.InitPreviewResponse;
|
||||
import com.cisd.tms.modules.init.dto.InitTaskDetailResponse;
|
||||
import com.cisd.tms.modules.init.dto.InitTaskExecuteResponse;
|
||||
import com.cisd.tms.modules.init.dto.InitTaskStepLogResponse;
|
||||
import com.cisd.tms.modules.init.dto.InitTaskStepResponse;
|
||||
import com.cisd.tms.modules.init.dto.ResetCreateTaskResponse;
|
||||
import com.cisd.tms.modules.init.dto.ResetPreviewResponse;
|
||||
import com.cisd.tms.modules.init.dto.*;
|
||||
import com.cisd.tms.modules.init.service.InitService;
|
||||
import com.cisd.tms.modules.log.annotation.AuditedOperation;
|
||||
import com.cisd.tms.modules.log.enums.ActionType;
|
||||
import com.cisd.tms.modules.log.enums.ModuleCode;
|
||||
import com.cisd.tms.security.internal.ReplayProtected;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/init")
|
||||
@ -66,6 +54,7 @@ public class InitController {
|
||||
@PostMapping("/tasks")
|
||||
@Operation(summary = "创建初始化任务", description = "根据初始化请求生成任务和步骤记录,任务初始状态为 PENDING。")
|
||||
@ReplayProtected
|
||||
@AuditedOperation(module = ModuleCode.INIT, action = ActionType.CREATE, summary = "创建初始化任务")
|
||||
public ApiResponse<InitCreateTaskResponse> createTask(@Valid @RequestBody InitPreviewRequest request) {
|
||||
return ApiResponse.success(initService.createTask(request));
|
||||
}
|
||||
@ -115,6 +104,7 @@ public class InitController {
|
||||
@PostMapping("/tasks/{taskId}/execute")
|
||||
@Operation(summary = "执行初始化任务", description = "异步受理初始化任务执行请求,立即返回当前任务快照,实际步骤在后台继续运行。")
|
||||
@ReplayProtected
|
||||
@AuditedOperation(module = ModuleCode.INIT, action = ActionType.EXECUTE, summary = "执行初始化任务")
|
||||
public ApiResponse<InitTaskExecuteResponse> executeTask(
|
||||
@Parameter(description = "初始化任务唯一标识")
|
||||
@PathVariable String taskId
|
||||
@ -131,6 +121,7 @@ public class InitController {
|
||||
@PostMapping("/reset/tasks")
|
||||
@Operation(summary = "创建重置任务", description = "自动读取当前设备有效初始化快照生成重置任务和步骤记录,任务初始状态为 PENDING。")
|
||||
@ReplayProtected
|
||||
@AuditedOperation(module = ModuleCode.INIT, action = ActionType.CREATE, summary = "创建重置任务")
|
||||
public ApiResponse<ResetCreateTaskResponse> createResetTask() {
|
||||
return ApiResponse.success(initService.createResetTask());
|
||||
}
|
||||
@ -138,6 +129,7 @@ public class InitController {
|
||||
@PostMapping("/reset/tasks/{taskId}/execute")
|
||||
@Operation(summary = "执行重置任务", description = "异步受理重置任务执行请求,立即返回当前任务快照,实际步骤在后台继续运行。")
|
||||
@ReplayProtected
|
||||
@AuditedOperation(module = ModuleCode.INIT, action = ActionType.EXECUTE, summary = "执行重置任务")
|
||||
public ApiResponse<InitTaskExecuteResponse> executeResetTask(
|
||||
@Parameter(description = "重置任务唯一标识")
|
||||
@PathVariable String taskId
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
package com.cisd.tms.modules.log.annotation;
|
||||
|
||||
import com.cisd.tms.modules.log.enums.ActionType;
|
||||
import com.cisd.tms.modules.log.enums.ModuleCode;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface AuditedOperation {
|
||||
ModuleCode module();
|
||||
ActionType action();
|
||||
String summary() default "";
|
||||
boolean sensitive() default true; // 默认都是敏感操作
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
package com.cisd.tms.modules.log.aspect;
|
||||
|
||||
import com.cisd.tms.modules.log.annotation.AuditedOperation;
|
||||
import com.cisd.tms.modules.log.dto.OperationAuditCommand;
|
||||
import com.cisd.tms.modules.log.enums.OperationResult;
|
||||
import com.cisd.tms.modules.log.service.OperationAuditService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
@Aspect
|
||||
@Component
|
||||
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;
|
||||
|
||||
@Around("@annotation(auditedOperation)")
|
||||
public Object doAround(ProceedingJoinPoint joinPoint, AuditedOperation auditedOperation) throws Throwable {
|
||||
OperationAuditCommand command = new OperationAuditCommand();
|
||||
command.setModuleCode(auditedOperation.module());
|
||||
command.setActionType(auditedOperation.action());
|
||||
command.setSummary(auditedOperation.summary());
|
||||
|
||||
//todo 后续测
|
||||
// 获取上下文信息
|
||||
getContextInfo(command);
|
||||
|
||||
Object result = null;
|
||||
try {
|
||||
result = joinPoint.proceed();
|
||||
command.setOperationResult(OperationResult.SUCCESS);
|
||||
return result;
|
||||
|
||||
} catch (Throwable e) {
|
||||
command.setOperationResult(OperationResult.FAILED);
|
||||
String errorMsg = e.getMessage();
|
||||
if (errorMsg != null && errorMsg.length() > 1000) {
|
||||
errorMsg = errorMsg.substring(0, 1000);
|
||||
}
|
||||
command.setErrorMessage(errorMsg);
|
||||
throw e;
|
||||
|
||||
} finally {
|
||||
// 最终一定会调用 record,由 record 内部决定是否因为 sensitive 抛出二次异常阻断
|
||||
auditService.record(command, auditedOperation.sensitive());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取请求上下文信息 (Role, AuthLevel, IP)
|
||||
*/
|
||||
private void getContextInfo(OperationAuditCommand command) {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes != null) {
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
|
||||
|
||||
Object roleObj = request.getAttribute(ATTR_ROLE_CODE);
|
||||
command.setOperatorRoleCode(roleObj != null ? roleObj.toString() : "UNKNOWN_ROLE");
|
||||
|
||||
Object levelObj = request.getAttribute(ATTR_AUTH_LEVEL);
|
||||
command.setOperatorAuthLevel(levelObj != null ? levelObj.toString() : "UNKNOWN_AUTH_LEVEL");
|
||||
|
||||
command.setRemoteIp(request.getRemoteAddr());
|
||||
} else {
|
||||
// 非 HTTP 请求上下文 (如定时任务触发)
|
||||
command.setOperatorRoleCode("UNKNOWN_ROLE");
|
||||
command.setOperatorAuthLevel("UNKNOWN_AUTH_LEVEL");
|
||||
command.setRemoteIp("127.0.0.1");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
package com.cisd.tms.modules.log.controller;
|
||||
|
||||
import com.cisd.tms.common.api.ApiResponse;
|
||||
import com.cisd.tms.modules.log.dto.BackupConfigRequest;
|
||||
import com.cisd.tms.modules.log.dto.BackupConfigResponse;
|
||||
import com.cisd.tms.modules.log.service.BackupConfigService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
|
||||
@Tag(name = "审计日志备份配置", description = "管理系统审计日志的定时备份策略")
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/api/v1/audit-logs/backup-config")
|
||||
public class BackupConfigController {
|
||||
|
||||
private final BackupConfigService backupConfigService;
|
||||
|
||||
@Operation(summary = "查询全局备份配置", description = "获取系统当前的审计日志备份策略,如果未配置过则返回 null")
|
||||
@GetMapping
|
||||
public ApiResponse<BackupConfigResponse> getConfig() {
|
||||
BackupConfigResponse config = backupConfigService.getConfig();
|
||||
return ApiResponse.success(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建或更新备份配置
|
||||
*/
|
||||
@Operation(summary = "保存或更新备份配置", description = "创建新的备份配置或覆盖更新已有的全局备份配置")
|
||||
@PostMapping
|
||||
public ApiResponse<Void> saveOrUpdateConfig(@Validated @RequestBody BackupConfigRequest req) {
|
||||
backupConfigService.saveOrUpdateConfig(req);
|
||||
return ApiResponse.success();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
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.modules.log.dto.AuditLogReviewRequest;
|
||||
import com.cisd.tms.modules.log.dto.OperationAuditLogPageRequest;
|
||||
import com.cisd.tms.modules.log.dto.OperationAuditLogPageResponse;
|
||||
import com.cisd.tms.modules.log.dto.OperationAuditLogResponse;
|
||||
import com.cisd.tms.modules.log.service.OperationAuditService;
|
||||
import com.cisd.tms.security.internal.InternalApiAuthInterceptor;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
@Tag(name = "审计日志管理", description = "提供系统操作审计日志的查询与审核功能")
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/api/v1/audit-logs")
|
||||
public class OperationAuditController {
|
||||
|
||||
private final OperationAuditService auditService;
|
||||
|
||||
@Operation(summary = "分页查询审计日志", description = "根据筛选条件分页获取系统的操作审计日志列表")
|
||||
@PostMapping("/page")
|
||||
public ApiResponse<IPage<OperationAuditLogPageResponse>> queryPage(@RequestBody OperationAuditLogPageRequest req) {
|
||||
IPage<OperationAuditLogPageResponse> pageResult = auditService.queryPage(req);
|
||||
return ApiResponse.success(pageResult);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取审计日志详情", description = "根据日志的唯一标识 ID 获取单条日志的完整信息")
|
||||
@GetMapping("/{logId}")
|
||||
public ApiResponse<OperationAuditLogResponse> getDetail(@PathVariable String logId) {
|
||||
OperationAuditLogResponse detail = auditService.getDetail(logId);
|
||||
return ApiResponse.success(detail);
|
||||
}
|
||||
|
||||
@Operation(summary = "审核审计日志", description = "审计管理员对特定的审计日志进行审核操作及意见批注")
|
||||
@PostMapping("/{logId}/review")
|
||||
public ApiResponse<Void> reviewLog(@PathVariable String logId, @RequestBody AuditLogReviewRequest req) {
|
||||
|
||||
String auditorUser = ""; // 默认
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes != null) {
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
Object roleObj = request.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE);
|
||||
|
||||
// 权限校验:仅审计管理员可操作
|
||||
if (roleObj == null || !roleObj.toString().equals("AUDIT_ADMIN")) {
|
||||
return ApiResponse.fail(403, "权限不足:仅审计管理员可执行此操作");
|
||||
}
|
||||
|
||||
auditorUser = roleObj.toString();
|
||||
}
|
||||
|
||||
auditService.reviewLog(logId, req, auditorUser);
|
||||
return ApiResponse.success();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package com.cisd.tms.modules.log.dto;
|
||||
|
||||
import com.cisd.tms.modules.log.enums.AuditResult;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "日志审计请求参数")
|
||||
public class AuditLogReviewRequest {
|
||||
|
||||
@Schema(description = "审计管理员签名值", example = "MEUC...")
|
||||
private String auditSign;
|
||||
@Schema(description = "审计结果 (PASS/ REJECT / NEED_VERIFY)", example = "PASS")
|
||||
private AuditResult auditResult;
|
||||
@Schema(description = "审计意见", example = "审核通过")
|
||||
private String auditComment;
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package com.cisd.tms.modules.log.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "备份策略配置请求参数")
|
||||
public class BackupConfigRequest {
|
||||
|
||||
@Schema(description = "启用状态 (1:启用 0:禁用)", example = "1", defaultValue = "1")
|
||||
private Integer enable = 1;
|
||||
|
||||
@Schema(description = "Cron定时表达式", example = "0 0 2 * * ?", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "Cron表达式不能为空")
|
||||
private String cronExp;
|
||||
|
||||
@Schema(description = "保留备份文件的份数", example = "7", defaultValue = "1")
|
||||
private Integer retentionCount = 1;
|
||||
}
|
||||
@ -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
|
||||
@Schema(description = "日志备份配置响应参数")
|
||||
public class BackupConfigResponse {
|
||||
|
||||
@Schema(description = "主键", example = "1")
|
||||
private Integer id;
|
||||
|
||||
@Schema(description = "启用状态 (1:启用 0:禁用)", example = "1")
|
||||
private Integer enable;
|
||||
|
||||
@Schema(description = "Cron定时表达式", example = "0 0 2 * * ?")
|
||||
private String cronExp;
|
||||
|
||||
@Schema(description = "保留备份文件的份数", example = "7")
|
||||
private Integer retentionCount;
|
||||
|
||||
@Schema(description = "最后一次备份时间", type = "string", example = "2026-04-16 09:10:16")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private LocalDateTime lastBackupTime;
|
||||
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package com.cisd.tms.modules.log.dto;
|
||||
|
||||
import com.cisd.tms.modules.log.enums.ActionType;
|
||||
import com.cisd.tms.modules.log.enums.ModuleCode;
|
||||
import com.cisd.tms.modules.log.enums.OperationResult;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class OperationAuditCommand {
|
||||
private String operatorRoleCode;
|
||||
private String operatorAuthLevel;
|
||||
private ModuleCode moduleCode;
|
||||
private ActionType actionType;
|
||||
private String remoteIp;
|
||||
private OperationResult operationResult;
|
||||
private String summary;
|
||||
private String errorMessage;
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
package com.cisd.tms.modules.log.dto;
|
||||
|
||||
import com.cisd.tms.modules.log.enums.*;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Schema(description = "操作审计日志分页查询请求参数")
|
||||
public class OperationAuditLogPageRequest {
|
||||
|
||||
@Schema(description = "操作人角色", example = "SYSTEM_ADMIN")
|
||||
private String operatorRoleCode;
|
||||
|
||||
@Schema(description = "系统模块", example = "DEVICE")
|
||||
private ModuleCode moduleCode;
|
||||
|
||||
@Schema(description = "操作类型 (如: ADD, UPDATE, DELETE, LOGIN)", example = "UPDATE")
|
||||
private ActionType actionType;
|
||||
|
||||
@Schema(description = "操作发起方IP地址", example = "192.168.1.1")
|
||||
private String remoteIp;
|
||||
|
||||
@Schema(description = "操作执行结果 (SUCCESS/ FAILED/ DENIED)", example = "SUCCESS")
|
||||
private OperationResult operationResult;
|
||||
|
||||
@Schema(description = "操作摘要说明", example = "修改了系统备份策略")
|
||||
private String summary;
|
||||
|
||||
@Schema(description = "审计状态 (PENDING/ REVIEWED)", example = "PENDING")
|
||||
private AuditStatus auditStatus;
|
||||
|
||||
@Schema(description = "审计结果 (PASS / REJECT/ NEED_VERIFY)", example = "PASS")
|
||||
private AuditResult auditResult;
|
||||
|
||||
@Schema(description = "查询开始时间", type = "string", format = "date-time", example = "2026-04-01 00:00:00")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private LocalDateTime dateFrom;
|
||||
|
||||
@Schema(description = "查询结束时间", type = "string", format = "date-time", example = "2026-04-16 23:59:59")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private LocalDateTime dateTo;
|
||||
|
||||
@Schema(description = "全局模糊搜索关键字 (可匹配操作人、模块、摘要等)", example = "备份")
|
||||
private String keyword;
|
||||
|
||||
@Schema(description = "当前页码", example = "1", defaultValue = "1", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private int pageNum = 1;
|
||||
|
||||
@Schema(description = "每页显示条数", example = "10", defaultValue = "10", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private int pageSize = 10;
|
||||
|
||||
// @Schema(description = "记录落库时间", type = "string", format = "date-time")
|
||||
// private Date createTime;
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
package com.cisd.tms.modules.log.dto;
|
||||
|
||||
import com.cisd.tms.modules.log.enums.*;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Schema(description = "操作审计日志分页列表响应对象")
|
||||
public class OperationAuditLogPageResponse {
|
||||
|
||||
@Schema(description = "日志唯一标识ID", example = "1791234567890123456")
|
||||
private String logId;
|
||||
|
||||
@Schema(description = "操作人角色", example = "SYSTEM_ADMIN")
|
||||
private String operatorRoleCode;
|
||||
|
||||
@Schema(description = "系统模块", example = "DEVICE")
|
||||
private ModuleCode moduleCode;
|
||||
|
||||
@Schema(description = "操作人认证等级", example = "FULL")
|
||||
private String operatorAuthLevel;
|
||||
|
||||
@Schema(description = "操作类型 (如: ADD, UPDATE, DELETE)", example = "UPDATE")
|
||||
private ActionType actionType;
|
||||
|
||||
@Schema(description = "操作发起方IP地址", example = "192.168.1.100")
|
||||
private String remoteIp;
|
||||
|
||||
@Schema(description = "操作执行结果 (SUCCESS/ FAILED/ DENIED)", example = "SUCCESS")
|
||||
private OperationResult operationResult;
|
||||
|
||||
@Schema(description = "操作摘要说明", example = "修改了系统备份策略配置")
|
||||
private String summary;
|
||||
|
||||
@Schema(description = "错误信息", example = "数据库连接超时")
|
||||
private String errorMessage;
|
||||
|
||||
@Schema(description = "审计状态 (PENDING / REVIEWED)", example = "REVIEWED")
|
||||
private AuditStatus auditStatus;
|
||||
|
||||
@Schema(description = "审计结果 (PASS/ REJECT / NEED_VERIFY)", example = "PASS")
|
||||
private AuditResult auditResult;
|
||||
|
||||
@Schema(description = "审计意见", example = "审核通过")
|
||||
private String auditComment;
|
||||
|
||||
@Schema(description = "审计人", example = "AUDIT_ADMIN")
|
||||
private String auditedBy;
|
||||
|
||||
@Schema(description = "审核完成时间", type = "string", example = "2026-04-16 10:00:00")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private LocalDateTime auditedAt;
|
||||
|
||||
// @Schema(description = "请求数据体 SM3 摘要", example = "1ab2c3d4e5f6...")
|
||||
// private String payloadHash; // SM3摘要
|
||||
|
||||
@Schema(description = "请求 SM2 签名值", example = "MEUC...")
|
||||
private String signValue; // SM2签名值
|
||||
|
||||
@Schema(description = "操作实际发生时间", type = "string", example = "2026-04-16 09:10:16")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private LocalDateTime occurredAt; // 发生时间
|
||||
|
||||
// @Schema(description = "记录落库时间", type = "string", example = "2026-04-16 09:10:20")
|
||||
// @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
// private Date createTime; // 落库时间
|
||||
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
package com.cisd.tms.modules.log.dto;
|
||||
|
||||
import com.cisd.tms.modules.log.enums.*;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Schema(description = "操作审计日志详情响应对象")
|
||||
public class OperationAuditLogResponse {
|
||||
|
||||
@Schema(description = "操作人角色代码", example = "SYSTEM_ADMIN")
|
||||
private String operatorRoleCode;
|
||||
|
||||
@Schema(description = "操作人认证等级", example = "LEVEL_2")
|
||||
private String operatorAuthLevel;
|
||||
|
||||
@Schema(description = "系统模块代码", example = "LOG_MANAGEMENT")
|
||||
private ModuleCode moduleCode;
|
||||
|
||||
@Schema(description = "操作类型", example = "UPDATE")
|
||||
private ActionType actionType;
|
||||
|
||||
@Schema(description = "操作发起方IP地址", example = "192.168.1.100")
|
||||
private String remoteIp;
|
||||
|
||||
@Schema(description = "操作执行结果 (SUCCESS/ FAILED / DENIED)", example = "SUCCESS")
|
||||
private OperationResult operationResult;
|
||||
|
||||
@Schema(description = "操作摘要说明", example = "修改了系统备份策略配置")
|
||||
private String summary;
|
||||
|
||||
@Schema(description = "错误信息(仅操作失败时存在)", example = "数据库连接超时")
|
||||
private String errorMessage;
|
||||
|
||||
@Schema(description = "审计状态 (PENDING / REVIEWED)", example = "REVIEWED")
|
||||
private AuditStatus auditStatus;
|
||||
|
||||
@Schema(description = "审计结果 (PASS/ REJECT/ NEED_VERIFY)", example = "PASS")
|
||||
private AuditResult auditResult;
|
||||
|
||||
@Schema(description = "审计意见", example = "审核通过")
|
||||
private String auditComment;
|
||||
|
||||
@Schema(description = "审计人", example = "AUDIT_ADMIN")
|
||||
private String auditedBy;
|
||||
|
||||
@Schema(description = "审计完成时间", type = "string", format = "date-time", example = "2026-04-16 23:59:59")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private LocalDateTime auditedAt;
|
||||
|
||||
// @Schema(description = "请求数据体 SM3 摘要", example = "1ab2c3d4e5f6...")
|
||||
// private String payloadHash; // SM3摘要
|
||||
|
||||
@Schema(description = "请求 SM2 签名值", example = "MEUC...")
|
||||
private String signValue; // SM2签名值
|
||||
|
||||
@Schema(description = "操作实际发生时间", type = "string", format = "date-time", example = "2026-04-16 23:59:59")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private LocalDateTime occurredAt; // 发生时间
|
||||
|
||||
// @Schema(description = "记录落库时间", type = "string", format = "date-time", example = "2026-04-16T09:10:20")
|
||||
// private Date createTime; // 落库时间
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
package com.cisd.tms.modules.log.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 审计日志备份策略配置实体类
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@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 LocalDateTime lastBackupTime;
|
||||
|
||||
/**
|
||||
* 其他字段的签名值 (防篡改)
|
||||
*/
|
||||
private String signData;
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
package com.cisd.tms.modules.log.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@TableName("tms_log_backup")
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class BackupRecordEntity {
|
||||
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 文件名
|
||||
*/
|
||||
private String fileName;
|
||||
|
||||
/**
|
||||
* 存储路径
|
||||
*/
|
||||
private String filePath;
|
||||
|
||||
/**
|
||||
* 文件哈希
|
||||
*/
|
||||
private String fileHash;
|
||||
|
||||
/**
|
||||
* 文件签名
|
||||
*/
|
||||
private String fileSign;
|
||||
|
||||
/**
|
||||
* 状态 SUCCESS/DELETED
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 其他字段签名值
|
||||
*/
|
||||
private String signData;
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package com.cisd.tms.modules.log.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.cisd.tms.modules.log.enums.*;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@TableName("tms_operation_audit_log")
|
||||
public class OperationAuditLogEntity {
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private String logId;
|
||||
private String operatorRoleCode;
|
||||
private String operatorAuthLevel;
|
||||
private ModuleCode moduleCode;
|
||||
private ActionType actionType;
|
||||
private String remoteIp;
|
||||
private OperationResult operationResult;
|
||||
private String summary;
|
||||
private String errorMessage;
|
||||
private AuditStatus auditStatus;
|
||||
private AuditResult auditResult;
|
||||
private String auditComment;
|
||||
private String auditedBy;
|
||||
private LocalDateTime auditedAt;
|
||||
private String payloadHash; // SM3摘要
|
||||
private String signValue; // SM2签名值
|
||||
private LocalDateTime occurredAt; // 发生时间
|
||||
private LocalDateTime createTime; // 落库时间
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package com.cisd.tms.modules.log.enums;
|
||||
|
||||
public enum ActionType {
|
||||
LOGIN, LOGOUT, CREATE, UPDATE, DELETE, EXECUTE, RESET, ENABLE, DISABLE, BIND, IMPORT, EXPORT, BACKUP, RECOVER, RESTART, ACCESS, ROLLBACK
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package com.cisd.tms.modules.log.enums;
|
||||
|
||||
public enum AuditResult {
|
||||
PASS,
|
||||
REJECT,
|
||||
NEED_VERIFY
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package com.cisd.tms.modules.log.enums;
|
||||
|
||||
public enum AuditStatus {
|
||||
PENDING, REVIEWED
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package com.cisd.tms.modules.log.enums;
|
||||
|
||||
public enum ModuleCode {
|
||||
AUTH, INIT, UPGRADE, DEVICE, NETWORK, KEY, SYSTEM, SECURITY
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package com.cisd.tms.modules.log.enums;
|
||||
|
||||
public enum OperationResult {
|
||||
SUCCESS, FAILED, DENIED
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.cisd.tms.modules.log.mapper;
|
||||
|
||||
import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX;
|
||||
import com.cisd.tms.modules.log.entity.BackupConfigEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface BackupConfigMapper extends BaseMapperX<BackupConfigEntity> {
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.cisd.tms.modules.log.mapper;
|
||||
|
||||
import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX;
|
||||
import com.cisd.tms.modules.log.entity.BackupRecordEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface BackupRecordMapper extends BaseMapperX<BackupRecordEntity> {
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
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;
|
||||
|
||||
@Mapper
|
||||
public interface OperationAuditLogMapper extends BaseMapperX<OperationAuditLogEntity> {
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package com.cisd.tms.modules.log.repository;
|
||||
|
||||
import com.cisd.tms.modules.log.entity.BackupConfigEntity;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
|
||||
public interface BackupConfigRepository {
|
||||
Optional<BackupConfigEntity> find();
|
||||
void saveOrUpdate(BackupConfigEntity backupConfig);
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package com.cisd.tms.modules.log.repository;
|
||||
|
||||
import com.cisd.tms.modules.log.entity.BackupRecordEntity;
|
||||
|
||||
public interface BackupRecordRepository {
|
||||
void add(BackupRecordEntity backupRecordEntity);
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package com.cisd.tms.modules.log.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.cisd.tms.modules.log.dto.OperationAuditLogPageRequest;
|
||||
import com.cisd.tms.modules.log.entity.OperationAuditLogEntity;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface OperationAuditLogRepository {
|
||||
void add(OperationAuditLogEntity auditLogEntity);
|
||||
IPage<OperationAuditLogEntity> findPage(OperationAuditLogPageRequest req);
|
||||
Optional<OperationAuditLogEntity> findByLogId(String logId);
|
||||
void update(OperationAuditLogEntity entity);
|
||||
Optional<List<OperationAuditLogEntity>> findByAuditStatus (String auditStatus);
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
package com.cisd.tms.modules.log.repository.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.cisd.tms.modules.log.entity.BackupConfigEntity;
|
||||
import com.cisd.tms.modules.log.mapper.BackupConfigMapper;
|
||||
import com.cisd.tms.modules.log.repository.BackupConfigRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public class BackupConfigRepositoryImpl implements BackupConfigRepository {
|
||||
|
||||
private final BackupConfigMapper backupConfigMapper;
|
||||
public BackupConfigRepositoryImpl(BackupConfigMapper backupConfigMapper) {
|
||||
this.backupConfigMapper = backupConfigMapper;
|
||||
}
|
||||
@Override
|
||||
public Optional<BackupConfigEntity> find(){
|
||||
LambdaQueryWrapper<BackupConfigEntity> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
wrapper.last("LIMIT 1");
|
||||
|
||||
BackupConfigEntity entity = backupConfigMapper.selectOne(wrapper);
|
||||
return Optional.ofNullable(entity);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void saveOrUpdate (BackupConfigEntity backupConfig) {
|
||||
if (backupConfig.getId() == null) {
|
||||
backupConfigMapper.insert(backupConfig);
|
||||
} else {
|
||||
backupConfigMapper.updateById(backupConfig);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package com.cisd.tms.modules.log.repository.impl;
|
||||
|
||||
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;
|
||||
|
||||
@Repository
|
||||
public class BackupRecordRepositoryImpl implements BackupRecordRepository {
|
||||
|
||||
public final BackupRecordMapper backupRecordMapper;
|
||||
|
||||
public BackupRecordRepositoryImpl(BackupRecordMapper backupRecordMapper) {
|
||||
this.backupRecordMapper = backupRecordMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(BackupRecordEntity backupRecordEntity){
|
||||
backupRecordMapper.insert(backupRecordEntity);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
package com.cisd.tms.modules.log.repository.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
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.OperationAuditLogPageRequest;
|
||||
import com.cisd.tms.modules.log.entity.OperationAuditLogEntity;
|
||||
import com.cisd.tms.modules.log.mapper.OperationAuditLogMapper;
|
||||
import com.cisd.tms.modules.log.repository.OperationAuditLogRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
|
||||
@Repository
|
||||
public class OperationAuditLogRepositoryImpl implements OperationAuditLogRepository {
|
||||
|
||||
private final OperationAuditLogMapper operationAuditLogMapper;
|
||||
public OperationAuditLogRepositoryImpl(OperationAuditLogMapper operationAuditLogMapper) {
|
||||
this.operationAuditLogMapper= operationAuditLogMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(OperationAuditLogEntity operationAuditLogEntity) {
|
||||
operationAuditLogMapper.insert(operationAuditLogEntity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<OperationAuditLogEntity> findPage(OperationAuditLogPageRequest req){
|
||||
IPage<OperationAuditLogEntity> page = new Page<>(req.getPageNum(), req.getPageSize());
|
||||
LambdaQueryWrapper<OperationAuditLogEntity> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
wrapper.eq(StringUtils.isNotBlank(req.getOperatorRoleCode()), OperationAuditLogEntity::getOperatorRoleCode, req.getOperatorRoleCode())
|
||||
.eq(req.getModuleCode() != null, OperationAuditLogEntity::getModuleCode, req.getModuleCode())
|
||||
.eq(req.getActionType() != null, OperationAuditLogEntity::getActionType, req.getActionType())
|
||||
.eq(req.getOperationResult() != null, OperationAuditLogEntity::getOperationResult, req.getOperationResult())
|
||||
.eq(req.getAuditStatus() != null, OperationAuditLogEntity::getAuditStatus, req.getAuditStatus())
|
||||
.eq(req.getAuditResult() != null, OperationAuditLogEntity::getAuditResult, req.getAuditResult())
|
||||
.eq(StringUtils.isNotBlank(req.getRemoteIp()), OperationAuditLogEntity::getRemoteIp, req.getRemoteIp())
|
||||
.eq(StringUtils.isNotBlank(req.getKeyword()), OperationAuditLogEntity::getSummary, req.getKeyword());
|
||||
|
||||
if (req.getDateFrom() != null) {
|
||||
wrapper.ge(OperationAuditLogEntity::getOccurredAt, req.getDateFrom());
|
||||
}
|
||||
if (req.getDateTo() != null) {
|
||||
wrapper.le(OperationAuditLogEntity::getOccurredAt, req.getDateTo());
|
||||
}
|
||||
|
||||
// 搜索摘要
|
||||
if (StringUtils.isNotBlank(req.getKeyword())) {
|
||||
wrapper.like(OperationAuditLogEntity::getSummary, req.getKeyword());
|
||||
}
|
||||
|
||||
wrapper.orderByDesc(OperationAuditLogEntity::getOccurredAt);
|
||||
|
||||
IPage<OperationAuditLogEntity> entityPage = operationAuditLogMapper.selectPage(page, wrapper);
|
||||
|
||||
return entityPage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<OperationAuditLogEntity> findByLogId(String logId){
|
||||
LambdaQueryWrapper<OperationAuditLogEntity> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(OperationAuditLogEntity::getLogId, logId);
|
||||
OperationAuditLogEntity entity = operationAuditLogMapper.selectOne(wrapper);
|
||||
return Optional.ofNullable(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(OperationAuditLogEntity entity) {
|
||||
operationAuditLogMapper.updateById(entity);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Optional<List<OperationAuditLogEntity>> findByAuditStatus(String auditStatus) {
|
||||
QueryWrapper<OperationAuditLogEntity> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("audit_status", auditStatus);
|
||||
List<OperationAuditLogEntity> logList = operationAuditLogMapper.selectList(queryWrapper);
|
||||
return Optional.ofNullable(logList);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
package com.cisd.tms.modules.log.service;
|
||||
|
||||
|
||||
import com.cisd.tms.modules.log.dto.BackupConfigRequest;
|
||||
import com.cisd.tms.modules.log.dto.BackupConfigResponse;
|
||||
import com.cisd.tms.modules.log.entity.BackupConfigEntity;
|
||||
import com.cisd.tms.modules.log.repository.BackupConfigRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.scheduling.support.CronExpression;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class BackupConfigService {
|
||||
|
||||
private final BackupConfigRepository backupConfigRepository;
|
||||
|
||||
public BackupConfigResponse getConfig(){
|
||||
BackupConfigEntity entity = backupConfigRepository.find().orElse(null);
|
||||
BackupConfigResponse response = new BackupConfigResponse();
|
||||
if (entity != null){
|
||||
response.setId(entity.getId());
|
||||
response.setEnable(entity.getEnable());
|
||||
response.setCronExp(entity.getCronExp());
|
||||
response.setLastBackupTime(entity.getLastBackupTime());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
public void saveOrUpdateConfig(BackupConfigRequest req){
|
||||
String cronExp = trim(req.getCronExp());
|
||||
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());
|
||||
}
|
||||
|
||||
entity.setEnable(req.getEnable());
|
||||
entity.setCronExp(req.getCronExp());
|
||||
entity.setRetentionCount(req.getRetentionCount());
|
||||
backupConfigRepository.saveOrUpdate(entity);
|
||||
}
|
||||
|
||||
private String trim(String str) {
|
||||
return str == null ? "" : str.trim();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,231 @@
|
||||
package com.cisd.tms.modules.log.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.cisd.tms.common.enums.ErrorCode;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.EccInternalVerifyRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
|
||||
import com.cisd.tms.modules.log.annotation.AuditedOperation;
|
||||
import com.cisd.tms.modules.log.dto.*;
|
||||
import com.cisd.tms.modules.log.entity.OperationAuditLogEntity;
|
||||
import com.cisd.tms.modules.log.enums.AuditStatus;
|
||||
import com.cisd.tms.modules.log.enums.OperationResult;
|
||||
import com.cisd.tms.modules.log.repository.OperationAuditLogRepository;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Base64;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class OperationAuditService {
|
||||
|
||||
public static final String ATTR_ROLE_CODE = "CURRENT_ROLE_CODE";
|
||||
public static final String ATTR_AUTH_LEVEL = "CURRENT_AUTH_LEVEL";
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(OperationAuditService.class);
|
||||
|
||||
|
||||
private final OperationAuditLogRepository operationAuditLogRepository;
|
||||
|
||||
private final OperationAuditSigner auditSigner;
|
||||
|
||||
private final PcieCryptoService pcieCryptoService;
|
||||
|
||||
public void record(OperationAuditCommand command, boolean isSensitive) {
|
||||
try {
|
||||
OperationAuditLogEntity entity = new OperationAuditLogEntity();
|
||||
|
||||
entity.setOperatorRoleCode(trim(command.getOperatorRoleCode()))
|
||||
.setOperatorAuthLevel(trim(command.getOperatorAuthLevel()))
|
||||
.setModuleCode(command.getModuleCode())
|
||||
.setActionType(command.getActionType())
|
||||
.setRemoteIp(trim(command.getRemoteIp()))
|
||||
.setOperationResult(command.getOperationResult())
|
||||
.setSummary(trim(command.getSummary()))
|
||||
.setErrorMessage(trim(command.getErrorMessage()));
|
||||
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
entity.setLogId(UUID.randomUUID().toString().replace("-", ""))
|
||||
.setOccurredAt(now)
|
||||
.setCreateTime(now)
|
||||
.setAuditStatus(AuditStatus.PENDING);
|
||||
|
||||
//todo 后续测试签名
|
||||
auditSigner.signEntity(entity);
|
||||
|
||||
operationAuditLogRepository.add(entity);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("审计日志安全写入失败: {}", e.getMessage(), e);
|
||||
// 敏感操作打断业务
|
||||
if (isSensitive) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "安全审计日志写入失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void recordDeniedLog(HttpServletRequest request, HandlerMethod handlerMethod, String errorMsg) {
|
||||
|
||||
AuditedOperation auditedOperation = findAnnotation(handlerMethod, AuditedOperation.class);
|
||||
|
||||
if (auditedOperation != null) {
|
||||
OperationAuditCommand command = new OperationAuditCommand();
|
||||
command.setModuleCode(auditedOperation.module());
|
||||
command.setActionType(auditedOperation.action());
|
||||
command.setSummary(auditedOperation.summary());
|
||||
|
||||
// 从上下文中获取刚才解析出的用户信息
|
||||
Object roleObj = request.getAttribute(ATTR_ROLE_CODE);
|
||||
command.setOperatorRoleCode(roleObj != null ? roleObj.toString() : "UNKNOWN");
|
||||
|
||||
Object levelObj = request.getAttribute(ATTR_AUTH_LEVEL);
|
||||
command.setOperatorAuthLevel(levelObj != null ? levelObj.toString() : "UNKNOWN");
|
||||
|
||||
command.setRemoteIp(request.getRemoteAddr());
|
||||
command.setErrorMessage(errorMsg);
|
||||
|
||||
command.setOperationResult(OperationResult.DENIED);
|
||||
|
||||
try {
|
||||
record(command, false);
|
||||
} catch (Exception e) {
|
||||
log.error("拦截器记录越权审计日志失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private <T extends java.lang.annotation.Annotation> T findAnnotation(HandlerMethod handlerMethod, Class<T> annotationType) {
|
||||
T methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getMethod(), annotationType);
|
||||
if (methodAnnotation != null) {
|
||||
return methodAnnotation;
|
||||
}
|
||||
return AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getBeanType(), annotationType);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public IPage<OperationAuditLogPageResponse> queryPage(OperationAuditLogPageRequest req) {
|
||||
|
||||
|
||||
IPage<OperationAuditLogEntity> entityPage = operationAuditLogRepository.findPage(req);
|
||||
|
||||
return entityPage.convert(entity -> {
|
||||
OperationAuditLogPageResponse resp = new OperationAuditLogPageResponse();
|
||||
resp.setLogId(entity.getLogId());
|
||||
resp.setOperatorAuthLevel(entity.getOperatorAuthLevel());
|
||||
resp.setOperatorRoleCode(entity.getOperatorRoleCode());
|
||||
resp.setModuleCode(entity.getModuleCode());
|
||||
resp.setActionType(entity.getActionType());
|
||||
resp.setOperationResult(entity.getOperationResult());
|
||||
resp.setErrorMessage(entity.getErrorMessage());
|
||||
resp.setAuditStatus(entity.getAuditStatus());
|
||||
resp.setAuditResult(entity.getAuditResult());
|
||||
resp.setAuditComment(entity.getAuditComment());
|
||||
resp.setAuditedBy(entity.getAuditedBy());
|
||||
resp.setRemoteIp(entity.getRemoteIp());
|
||||
resp.setErrorMessage(entity.getErrorMessage());
|
||||
resp.setOccurredAt(entity.getOccurredAt());
|
||||
return resp;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
public OperationAuditLogResponse getDetail(String logId) {
|
||||
logId = trim(logId);
|
||||
if (logId.isEmpty()){
|
||||
throw new IllegalArgumentException("logId不能为空");
|
||||
}
|
||||
|
||||
OperationAuditLogEntity entity = operationAuditLogRepository.findByLogId(logId)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "未找到该logId"));
|
||||
|
||||
OperationAuditLogResponse resp = new OperationAuditLogResponse();
|
||||
BeanUtils.copyProperties(entity, resp);
|
||||
return resp;
|
||||
}
|
||||
|
||||
|
||||
public void reviewLog(String logId, AuditLogReviewRequest req, String auditorUser) {
|
||||
|
||||
logId = trim(logId);
|
||||
if (logId.isEmpty()){
|
||||
throw new IllegalArgumentException("logId不能为空");
|
||||
}
|
||||
|
||||
OperationAuditLogEntity existLog = operationAuditLogRepository.findByLogId(logId)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "未找到该logId"));
|
||||
|
||||
String signValue = trim(req.getAuditSign());
|
||||
if (signValue.isEmpty()){
|
||||
throw new IllegalArgumentException("签名值不能为空");
|
||||
}
|
||||
|
||||
//todo 验签
|
||||
EccInternalVerifyRequest VerifyRequest = new EccInternalVerifyRequest();
|
||||
// todo后续更改
|
||||
VerifyRequest.setKeyIndex(1);
|
||||
VerifyRequest.setData(req.getAuditSign().getBytes(StandardCharsets.UTF_8));
|
||||
VerifyRequest.setSignature(decodeBlob(signValue, "signature is invalid"));
|
||||
try {
|
||||
pcieCryptoService.eccInternalVerify(VerifyRequest);
|
||||
} catch (BizException ex) {
|
||||
throw ex;
|
||||
} catch (RuntimeException ex) {
|
||||
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "signature verification failed");
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (!AuditStatus.PENDING.equals(existLog.getAuditStatus())) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "该日志已审计,禁止重复操作");
|
||||
}
|
||||
|
||||
OperationAuditLogEntity updateEntity = new OperationAuditLogEntity();
|
||||
updateEntity.setLogId(existLog.getLogId());
|
||||
updateEntity.setAuditStatus(AuditStatus.REVIEWED);
|
||||
updateEntity.setAuditResult(req.getAuditResult());
|
||||
updateEntity.setAuditComment(trim(req.getAuditComment()));
|
||||
updateEntity.setAuditedBy(auditorUser);
|
||||
updateEntity.setAuditedAt(LocalDateTime.now());
|
||||
|
||||
// 4. 执行更新
|
||||
operationAuditLogRepository.update(updateEntity);
|
||||
|
||||
log.info("日志复核完成, logId: {}, 审计人: {}, 结果: {}, {}", logId, auditorUser, req.getAuditResult(), req.getAuditComment());
|
||||
}
|
||||
|
||||
|
||||
private byte[] decodeBlob(String value, String message) {
|
||||
String normalized = value == null ? "" : value.trim();
|
||||
if (normalized.isEmpty()) {
|
||||
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), message);
|
||||
}
|
||||
if (normalized.matches("(?i)^[0-9a-f]+$") && normalized.length() % 2 == 0) {
|
||||
return Hex.decode(normalized);
|
||||
}
|
||||
try {
|
||||
return Base64.getDecoder().decode(normalized);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private String trim(String s){
|
||||
return s == null ? "" : s.trim();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
package com.cisd.tms.modules.log.service;
|
||||
|
||||
import com.cisd.tms.integration.crypto.pcie.Gm0018AlgorithmIds;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.DigestRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.EccInternalSignRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
|
||||
import com.cisd.tms.modules.log.entity.OperationAuditLogEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Base64;
|
||||
|
||||
@Component
|
||||
public class OperationAuditSigner {
|
||||
|
||||
|
||||
private final PcieCryptoService pcieCryptoService;
|
||||
OperationAuditSigner(PcieCryptoService pcieCryptoService) {
|
||||
this.pcieCryptoService = pcieCryptoService;
|
||||
}
|
||||
|
||||
public String buildAuditPayload(OperationAuditLogEntity entity) {
|
||||
return "log_id=" + trim(entity.getLogId()) + "\n" +
|
||||
"operator_role_code=" + trim(entity.getOperatorRoleCode()) + "\n" +
|
||||
"operator_auth_level=" + trim(entity.getOperatorAuthLevel()) + "\n" +
|
||||
"module_code=" + entity.getModuleCode() + "\n" +
|
||||
"action_type=" + entity.getActionType() + "\n" +
|
||||
"remote_ip=" + trim(entity.getRemoteIp()) + "\n" +
|
||||
"operation_result=" + entity.getOperationResult() + "\n" +
|
||||
"summary=" + trim(entity.getSummary()) + "\n" +
|
||||
"occurred_at=" + formatUtc(entity.getOccurredAt());
|
||||
}
|
||||
|
||||
|
||||
//todo 生成签名
|
||||
public void signEntity(OperationAuditLogEntity entity) {
|
||||
String payload = buildAuditPayload(entity);
|
||||
|
||||
//计算SM3哈希
|
||||
DigestRequest request = new DigestRequest();
|
||||
request.setAlgId(Gm0018AlgorithmIds.SM3);
|
||||
request.setData(payload.getBytes(StandardCharsets.UTF_8));
|
||||
BackupDataResult payloadHash = pcieCryptoService.digest(request);
|
||||
|
||||
entity.setPayloadHash(Base64.getEncoder().encodeToString(payloadHash.getData()));
|
||||
|
||||
// 3. 计算 SM2 签名值
|
||||
EccInternalSignRequest signRequest = new EccInternalSignRequest();
|
||||
signRequest.setData(payloadHash.getData());
|
||||
signRequest.setKeyIndex(1);
|
||||
BackupDataResult sign = pcieCryptoService.eccInternalSign(signRequest);
|
||||
entity.setSignValue(Base64.getEncoder().encodeToString(sign.getData()));
|
||||
}
|
||||
|
||||
|
||||
private String formatUtc(LocalDateTime date) {
|
||||
if (date == null) {
|
||||
return "";
|
||||
}
|
||||
ZonedDateTime utcTime = date.atZone(ZoneOffset.UTC);
|
||||
return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")
|
||||
.format(utcTime);
|
||||
}
|
||||
|
||||
private String trim(String str) {
|
||||
return str == null ? "" : str.trim();
|
||||
}
|
||||
}
|
||||
@ -3,13 +3,10 @@ package com.cisd.tms.modules.upgrade.controller;
|
||||
import com.cisd.tms.common.api.ApiResponse;
|
||||
import com.cisd.tms.modules.file.dto.FileUploadResponse;
|
||||
import com.cisd.tms.modules.file.service.FileService;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeCreateTaskRequest;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeCreateTaskResponse;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeLogResponse;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradePreviewRequest;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradePreviewResponse;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeTaskPageResponse;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeTaskResponse;
|
||||
import com.cisd.tms.modules.log.annotation.AuditedOperation;
|
||||
import com.cisd.tms.modules.log.enums.ActionType;
|
||||
import com.cisd.tms.modules.log.enums.ModuleCode;
|
||||
import com.cisd.tms.modules.upgrade.dto.*;
|
||||
import com.cisd.tms.modules.upgrade.service.UpgradeService;
|
||||
import com.cisd.tms.security.internal.ReplayProtected;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@ -17,13 +14,7 @@ import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
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.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@RestController
|
||||
@ -54,6 +45,7 @@ public class UpgradeController {
|
||||
@PostMapping("/upgrades")
|
||||
@Operation(summary = "创建离线升级任务", description = "根据 fileId 创建离线升级任务,状态初始为 PENDING_CONFIRM。")
|
||||
@ReplayProtected
|
||||
@AuditedOperation(module = ModuleCode.UPGRADE, action = ActionType.CREATE, summary = "创建离线升级任务")
|
||||
public ApiResponse<UpgradeCreateTaskResponse> create(@Valid @RequestBody UpgradeCreateTaskRequest request) {
|
||||
return ApiResponse.success(upgradeService.createTask(request));
|
||||
}
|
||||
@ -61,6 +53,7 @@ public class UpgradeController {
|
||||
@PostMapping("/upgrades/{taskId}/execute")
|
||||
@Operation(summary = "执行离线升级任务", description = "异步受理升级执行请求,立即返回任务当前快照。")
|
||||
@ReplayProtected
|
||||
@AuditedOperation(module = ModuleCode.UPGRADE, action = ActionType.EXECUTE, summary = "执行离线升级任务")
|
||||
public ApiResponse<UpgradeTaskResponse> execute(
|
||||
@Parameter(description = "升级任务号")
|
||||
@PathVariable String taskId
|
||||
@ -71,6 +64,7 @@ public class UpgradeController {
|
||||
@PostMapping("/upgrades/{taskId}/rollback")
|
||||
@Operation(summary = "回滚离线升级任务", description = "异步受理升级回滚请求,立即返回任务当前快照。")
|
||||
@ReplayProtected
|
||||
@AuditedOperation(module = ModuleCode.UPGRADE, action = ActionType.ROLLBACK, summary = "回滚离线升级任务")
|
||||
public ApiResponse<UpgradeTaskResponse> rollback(
|
||||
@Parameter(description = "升级任务号")
|
||||
@PathVariable String taskId
|
||||
|
||||
@ -1,84 +1,86 @@
|
||||
package com.cisd.tms.modules.auth.security;
|
||||
|
||||
import com.cisd.tms.security.internal.InternalApiAuthInterceptor;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.lang.reflect.Method;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
|
||||
class InternalAuthorizationInterceptorTest {
|
||||
|
||||
@Test
|
||||
void shouldAllowRequestWhenHandlerHasNoAuthorizationAnnotations() throws Exception {
|
||||
InternalAuthorizationInterceptor interceptor = new InternalAuthorizationInterceptor(new ObjectMapper());
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/v1/public");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
boolean allowed = interceptor.preHandle(request, response, handler("openEndpoint"));
|
||||
|
||||
Assertions.assertTrue(allowed);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectWhenRoleDoesNotMatchRequiredRole() throws Exception {
|
||||
InternalAuthorizationInterceptor interceptor = new InternalAuthorizationInterceptor(new ObjectMapper());
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/v1/auth/roles/AUDIT_ADMIN/enable");
|
||||
request.setAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "AUDIT_ADMIN");
|
||||
request.setAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "FULL");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
boolean allowed = interceptor.preHandle(request, response, handler("keyAdminFullEndpoint"));
|
||||
|
||||
Assertions.assertFalse(allowed);
|
||||
Assertions.assertEquals(403, response.getStatus());
|
||||
Assertions.assertTrue(response.getContentAsString().contains("role not allowed"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectWhenAuthLevelDoesNotMatchRequirement() throws Exception {
|
||||
InternalAuthorizationInterceptor interceptor = new InternalAuthorizationInterceptor(new ObjectMapper());
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/v1/auth/roles/AUDIT_ADMIN/reset-password");
|
||||
request.setAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "KEY_ADMIN");
|
||||
request.setAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "LIMITED");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
boolean allowed = interceptor.preHandle(request, response, handler("keyAdminFullEndpoint"));
|
||||
|
||||
Assertions.assertFalse(allowed);
|
||||
Assertions.assertEquals(403, response.getStatus());
|
||||
Assertions.assertTrue(response.getContentAsString().contains("auth level not allowed"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAllowWhenRoleAndAuthLevelMatch() throws Exception {
|
||||
InternalAuthorizationInterceptor interceptor = new InternalAuthorizationInterceptor(new ObjectMapper());
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/v1/auth/roles/AUDIT_ADMIN/reset-password");
|
||||
request.setAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "KEY_ADMIN");
|
||||
request.setAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "FULL");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
boolean allowed = interceptor.preHandle(request, response, handler("keyAdminFullEndpoint"));
|
||||
|
||||
Assertions.assertTrue(allowed);
|
||||
}
|
||||
|
||||
private HandlerMethod handler(String methodName) throws NoSuchMethodException {
|
||||
DemoController controller = new DemoController();
|
||||
Method method = DemoController.class.getDeclaredMethod(methodName);
|
||||
return new HandlerMethod(controller, method);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class DemoController {
|
||||
|
||||
public void openEndpoint() {
|
||||
}
|
||||
|
||||
@RequireInternalAuth(role = com.cisd.tms.modules.auth.enums.RoleCode.KEY_ADMIN, authLevel = com.cisd.tms.modules.auth.enums.AuthLevel.FULL)
|
||||
public void keyAdminFullEndpoint() {
|
||||
}
|
||||
}
|
||||
}
|
||||
//package com.cisd.tms.modules.auth.security;
|
||||
//
|
||||
//import com.cisd.tms.modules.log.service.OperationAuditService;
|
||||
//import com.cisd.tms.security.internal.InternalApiAuthInterceptor;
|
||||
//import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
//import org.junit.jupiter.api.Assertions;
|
||||
//import org.junit.jupiter.api.Test;
|
||||
//import org.springframework.mock.web.MockHttpServletRequest;
|
||||
//import org.springframework.mock.web.MockHttpServletResponse;
|
||||
//import org.springframework.web.method.HandlerMethod;
|
||||
//
|
||||
//import java.lang.reflect.Method;
|
||||
//
|
||||
//class InternalAuthorizationInterceptorTest {
|
||||
//
|
||||
// @Test
|
||||
// void shouldAllowRequestWhenHandlerHasNoAuthorizationAnnotations() throws Exception {
|
||||
// InternalAuthorizationInterceptor interceptor = new InternalAuthorizationInterceptor(new ObjectMapper());
|
||||
// MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/v1/public");
|
||||
// MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
//
|
||||
// boolean allowed = interceptor.preHandle(request, response, handler("openEndpoint"));
|
||||
//
|
||||
// Assertions.assertTrue(allowed);
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void shouldRejectWhenRoleDoesNotMatchRequiredRole() throws Exception {
|
||||
// InternalAuthorizationInterceptor interceptor = new InternalAuthorizationInterceptor(new ObjectMapper());
|
||||
// MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/v1/auth/roles/AUDIT_ADMIN/enable");
|
||||
// request.setAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "AUDIT_ADMIN");
|
||||
// request.setAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "FULL");
|
||||
// MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
//
|
||||
// boolean allowed = interceptor.preHandle(request, response, handler("keyAdminFullEndpoint"));
|
||||
//
|
||||
// Assertions.assertFalse(allowed);
|
||||
// Assertions.assertEquals(403, response.getStatus());
|
||||
// Assertions.assertTrue(response.getContentAsString().contains("role not allowed"));
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void shouldRejectWhenAuthLevelDoesNotMatchRequirement() throws Exception {
|
||||
// InternalAuthorizationInterceptor interceptor = new InternalAuthorizationInterceptor(new ObjectMapper());
|
||||
// MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/v1/auth/roles/AUDIT_ADMIN/reset-password");
|
||||
// request.setAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "KEY_ADMIN");
|
||||
// request.setAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "LIMITED");
|
||||
// MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
//
|
||||
// boolean allowed = interceptor.preHandle(request, response, handler("keyAdminFullEndpoint"));
|
||||
//
|
||||
// Assertions.assertFalse(allowed);
|
||||
// Assertions.assertEquals(403, response.getStatus());
|
||||
// Assertions.assertTrue(response.getContentAsString().contains("auth level not allowed"));
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void shouldAllowWhenRoleAndAuthLevelMatch() throws Exception {
|
||||
// InternalAuthorizationInterceptor interceptor = new InternalAuthorizationInterceptor(new ObjectMapper());
|
||||
// MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/v1/auth/roles/AUDIT_ADMIN/reset-password");
|
||||
// request.setAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "KEY_ADMIN");
|
||||
// request.setAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "FULL");
|
||||
// MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
//
|
||||
// boolean allowed = interceptor.preHandle(request, response, handler("keyAdminFullEndpoint"));
|
||||
//
|
||||
// Assertions.assertTrue(allowed);
|
||||
// }
|
||||
//
|
||||
// private HandlerMethod handler(String methodName) throws NoSuchMethodException {
|
||||
// DemoController controller = new DemoController();
|
||||
// Method method = DemoController.class.getDeclaredMethod(methodName);
|
||||
// return new HandlerMethod(controller, method);
|
||||
// }
|
||||
//
|
||||
// @SuppressWarnings("unused")
|
||||
// private static class DemoController {
|
||||
//
|
||||
// public void openEndpoint() {
|
||||
// }
|
||||
//
|
||||
// @RequireInternalAuth(role = com.cisd.tms.modules.auth.enums.RoleCode.KEY_ADMIN, authLevel = com.cisd.tms.modules.auth.enums.AuthLevel.FULL)
|
||||
// public void keyAdminFullEndpoint() {
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@ -0,0 +1,99 @@
|
||||
package com.cisd.tms.modules.log.aspect;
|
||||
|
||||
import com.cisd.tms.common.config.WebMvcConfig;
|
||||
import com.cisd.tms.modules.device.controller.NetworkConfigController;
|
||||
import com.cisd.tms.modules.device.service.NetworkConfigService;
|
||||
import com.cisd.tms.modules.log.dto.OperationAuditCommand;
|
||||
import com.cisd.tms.modules.log.service.OperationAuditService;
|
||||
import com.cisd.tms.security.internal.InternalApiAuthInterceptor;
|
||||
import com.cisd.tms.security.internal.InternalApiReplayInterceptor;
|
||||
import com.cisd.tms.security.openapi.OpenApiSignAuthInterceptor;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
||||
import org.springframework.context.annotation.FilterType;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@WebMvcTest(controllers = NetworkConfigController.class,
|
||||
excludeAutoConfiguration = {SecurityAutoConfiguration.class},
|
||||
excludeFilters = @ComponentScan.Filter(
|
||||
type = FilterType.ASSIGNABLE_TYPE,
|
||||
classes = {WebMvcConfig.class}
|
||||
))
|
||||
@Import(OperationAuditAspect.class)
|
||||
@EnableAspectJAutoProxy(proxyTargetClass = true)
|
||||
class OperationAuditAspectTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@MockitoBean
|
||||
private NetworkConfigService networkConfigService;
|
||||
|
||||
// 4. Mock 掉切面依赖的审计 Service (我们就是要验证它有没有被调用)
|
||||
@MockitoBean
|
||||
private OperationAuditService auditService;
|
||||
|
||||
@MockitoBean
|
||||
private InternalApiAuthInterceptor internalApiAuthInterceptor;
|
||||
|
||||
@MockitoBean
|
||||
private InternalApiReplayInterceptor internalApiReplayInterceptor;
|
||||
|
||||
@MockitoBean
|
||||
private OpenApiSignAuthInterceptor openApiSignAuthInterceptor;
|
||||
@Test
|
||||
void testAuditLog_OnSuccess() throws Exception {
|
||||
Mockito.when(networkConfigService.getNetworkInfo())
|
||||
.thenReturn(Collections.emptyList());
|
||||
|
||||
mockMvc.perform(get("/api/v1/device/network-config/network-info")
|
||||
.requestAttr(OperationAuditAspect.ATTR_ROLE_CODE, "SYS_ADMIN")
|
||||
.requestAttr(OperationAuditAspect.ATTR_AUTH_LEVEL, "FULL"))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
ArgumentCaptor<OperationAuditCommand> cmdCaptor = ArgumentCaptor.forClass(OperationAuditCommand.class);
|
||||
|
||||
Mockito.verify(auditService, Mockito.times(1))
|
||||
.record(cmdCaptor.capture(), eq(true));
|
||||
|
||||
OperationAuditCommand actualCmd = cmdCaptor.getValue();
|
||||
Assertions.assertEquals("NETWORK", actualCmd.getModuleCode(), "模块名应从注解中获取");
|
||||
Assertions.assertEquals("get", actualCmd.getActionType(), "动作类型应从注解中获取");
|
||||
Assertions.assertEquals("获取网络信息", actualCmd.getSummary(), "摘要应从注解中获取");
|
||||
Assertions.assertEquals("SUCCESS", actualCmd.getOperationResult(), "方法执行成功,结果应为 SUCCESS");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAuditLog_OnException() throws Exception {
|
||||
Mockito.when(networkConfigService.getNetworkInfo())
|
||||
.thenThrow(new RuntimeException("获取网卡信息超时报错啦!"));
|
||||
try {
|
||||
mockMvc.perform(get("/api/v1/device/network-config/network-info"));
|
||||
} catch (Exception e) {
|
||||
}
|
||||
|
||||
ArgumentCaptor<OperationAuditCommand> cmdCaptor = ArgumentCaptor.forClass(OperationAuditCommand.class);
|
||||
Mockito.verify(auditService, Mockito.times(1))
|
||||
.record(cmdCaptor.capture(), eq(true));
|
||||
|
||||
OperationAuditCommand actualCmd = cmdCaptor.getValue();
|
||||
Assertions.assertEquals("NETWORK", actualCmd.getModuleCode());
|
||||
Assertions.assertEquals("FAILED", actualCmd.getOperationResult(), "抛出异常,结果应为 FAILED");
|
||||
Assertions.assertEquals("获取网卡信息超时报错啦!", actualCmd.getErrorMessage(), "应该截获到异常信息");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,115 @@
|
||||
package com.cisd.tms.modules.log.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.cisd.tms.modules.log.dto.AuditLogReviewRequest;
|
||||
import com.cisd.tms.modules.log.dto.OperationAuditLogPageRequest;
|
||||
import com.cisd.tms.modules.log.dto.OperationAuditLogPageResponse;
|
||||
import com.cisd.tms.modules.log.dto.OperationAuditLogResponse;
|
||||
import com.cisd.tms.modules.log.enums.AuditResult;
|
||||
import com.cisd.tms.modules.log.enums.ModuleCode;
|
||||
import com.cisd.tms.modules.log.service.OperationAuditService;
|
||||
import com.cisd.tms.security.internal.InternalApiAuthInterceptor;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
class OperationAuditControllerTest {
|
||||
|
||||
private MockMvc mockMvc;
|
||||
private OperationAuditService auditService;
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
private final String BASE_URL = "/api/v1/audit-logs";
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
auditService = Mockito.mock(OperationAuditService.class);
|
||||
|
||||
objectMapper = new ObjectMapper();
|
||||
|
||||
mockMvc = MockMvcBuilders
|
||||
.standaloneSetup(new OperationAuditController(auditService))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testQueryPage_Success() throws Exception {
|
||||
OperationAuditLogPageRequest req = new OperationAuditLogPageRequest();
|
||||
req.setModuleCode(ModuleCode.NETWORK);
|
||||
req.setPageNum(1);
|
||||
req.setPageSize(10);
|
||||
|
||||
Page<OperationAuditLogPageResponse> mockPage = new Page<>(1, 10);
|
||||
OperationAuditLogPageResponse item = new OperationAuditLogPageResponse();
|
||||
item.setLogId("log-123");
|
||||
item.setModuleCode(ModuleCode.NETWORK);
|
||||
mockPage.setRecords(Collections.singletonList(item));
|
||||
mockPage.setTotal(1);
|
||||
|
||||
Mockito.when(auditService.queryPage(eq(req))).thenReturn(mockPage);
|
||||
|
||||
mockMvc.perform(post(BASE_URL)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andExpect(jsonPath("$.data.total").value(1))
|
||||
.andExpect(jsonPath("$.data.records[0].logId").value("log-123"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetDetail_Success() throws Exception {
|
||||
OperationAuditLogResponse mockDetail = new OperationAuditLogResponse();
|
||||
mockDetail.setOperatorRoleCode("SYS_ADMIN");
|
||||
|
||||
Mockito.when(auditService.getDetail("log-456")).thenReturn(mockDetail);
|
||||
|
||||
mockMvc.perform(get(BASE_URL + "/{logId}", "log-456"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.operatorRoleCode").value("SYS_ADMIN"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReviewLog_Success_WithAuditAdminRole() throws Exception {
|
||||
AuditLogReviewRequest req = new AuditLogReviewRequest();
|
||||
req.setAuditResult(AuditResult.PASS);
|
||||
req.setAuditSign("E1A2B3C4...");
|
||||
|
||||
Mockito.doNothing().when(auditService).reviewLog(eq("log-789"), any(), eq("AUDIT_ADMIN"));
|
||||
|
||||
mockMvc.perform(post(BASE_URL + "/{logId}/review", "log-789")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req))
|
||||
// 这里非常巧妙:就算没有拦截器,我们也能直接把 Attribute 塞进 Request 里供 Controller 读取
|
||||
.requestAttr(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "AUDIT_ADMIN"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReviewLog_Forbidden_WhenNotAuditAdmin() throws Exception {
|
||||
AuditLogReviewRequest req = new AuditLogReviewRequest();
|
||||
req.setAuditResult(AuditResult.PASS);
|
||||
|
||||
mockMvc.perform(post(BASE_URL + "/{logId}/review", "log-789")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(req))
|
||||
.requestAttr(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "SYS_ADMIN"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(403))
|
||||
.andExpect(jsonPath("$.msg").value("权限不足:仅审计管理员可执行此操作"));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package com.cisd.tms.modules.log.service;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class AuditBackupServiceTest {
|
||||
@Test
|
||||
public void FileTest(){
|
||||
String filePath = "./data/backup/audit_log_" + ".csv";
|
||||
|
||||
|
||||
File file = new File(filePath);
|
||||
System.out.print(file.getParentFile());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,132 @@
|
||||
package com.cisd.tms.modules.log.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
|
||||
import com.cisd.tms.modules.log.dto.OperationAuditLogPageRequest;
|
||||
import com.cisd.tms.modules.log.dto.OperationAuditLogPageResponse;
|
||||
import com.cisd.tms.modules.log.entity.OperationAuditLogEntity;
|
||||
import com.cisd.tms.modules.log.enums.ActionType;
|
||||
import com.cisd.tms.modules.log.enums.ModuleCode;
|
||||
import com.cisd.tms.modules.log.enums.OperationResult;
|
||||
import com.cisd.tms.modules.log.repository.OperationAuditLogRepository;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mock;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
class OperationAuditServiceTest {
|
||||
|
||||
@Mock
|
||||
private OperationAuditSigner auditSigner;
|
||||
|
||||
@Mock
|
||||
private PcieCryptoService pcieCryptoService;
|
||||
|
||||
@Test
|
||||
void shouldReturnPagedAndConvertedResponses() throws JsonProcessingException {
|
||||
InMemoryOperationAuditLogRepository fakeRepo = new InMemoryOperationAuditLogRepository();
|
||||
|
||||
fakeRepo.add(createMockEntity("log-1", ModuleCode.NETWORK, "SYS_ADMIN"));
|
||||
fakeRepo.add(createMockEntity("log-2", ModuleCode.NETWORK, "AUDIT_ADMIN"));
|
||||
fakeRepo.add(createMockEntity("log-3", ModuleCode.SYSTEM, "SYS_ADMIN"));
|
||||
|
||||
OperationAuditService service = new OperationAuditService(fakeRepo, auditSigner, pcieCryptoService);
|
||||
|
||||
OperationAuditLogPageRequest req = new OperationAuditLogPageRequest();
|
||||
req.setModuleCode(ModuleCode.NETWORK);
|
||||
req.setPageNum(1);
|
||||
req.setPageSize(10);
|
||||
|
||||
IPage<OperationAuditLogPageResponse> resultPage = service.queryPage(req);
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
System.out.println("========== 查询到的具体结果 ==========");
|
||||
String jsonResult = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(resultPage.getRecords());
|
||||
System.out.println(jsonResult);
|
||||
System.out.println("====================================");
|
||||
|
||||
Assertions.assertNotNull(resultPage);
|
||||
Assertions.assertEquals(1, resultPage.getTotal(), "总数应该只有2条 NETWORK 的日志");
|
||||
Assertions.assertEquals(2, resultPage.getRecords().size(), "当前页应该返回2条数据");
|
||||
|
||||
OperationAuditLogPageResponse firstRecord = resultPage.getRecords().get(0);
|
||||
Assertions.assertNotNull(firstRecord.getLogId());
|
||||
Assertions.assertEquals("NETWORK", firstRecord.getModuleCode());
|
||||
}
|
||||
|
||||
|
||||
|
||||
private OperationAuditLogEntity createMockEntity(String logId, ModuleCode moduleCode, String roleCode) {
|
||||
OperationAuditLogEntity entity = new OperationAuditLogEntity();
|
||||
entity.setLogId(logId);
|
||||
entity.setModuleCode(moduleCode);
|
||||
entity.setOperatorRoleCode(roleCode);
|
||||
entity.setActionType(ActionType.CREATE);
|
||||
entity.setOperationResult(OperationResult.SUCCESS);
|
||||
entity.setRemoteIp("127.0.0.1");
|
||||
return entity;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public class InMemoryOperationAuditLogRepository implements OperationAuditLogRepository {
|
||||
|
||||
// 用 Map 模拟数据库表,Key 是 logId
|
||||
private final Map<String, OperationAuditLogEntity> store = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void add(OperationAuditLogEntity entity) {
|
||||
store.put(entity.getLogId(), entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<OperationAuditLogEntity> findPage(OperationAuditLogPageRequest req) {
|
||||
List<OperationAuditLogEntity> filtered = store.values().stream()
|
||||
.filter(e -> req.getModuleCode() == null || req.getModuleCode().equals(e.getModuleCode()))
|
||||
.filter(e -> req.getOperatorRoleCode() == null || req.getOperatorRoleCode().equals(e.getOperatorRoleCode()))
|
||||
.filter(e -> req.getAuditStatus() == null || req.getAuditStatus().equals(e.getAuditStatus()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 2. 模拟 SQL 的 LIMIT 和 OFFSET 分页
|
||||
int current = req.getPageNum() > 0 ? req.getPageNum() : 1;
|
||||
int size = req.getPageSize() > 0 ? req.getPageSize() : 10;
|
||||
int start = (current - 1) * size;
|
||||
|
||||
List<OperationAuditLogEntity> pagedRecords = filtered.stream()
|
||||
.skip(start)
|
||||
.limit(size)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Page<OperationAuditLogEntity> page = new Page<>(current, size, filtered.size());
|
||||
page.setRecords(pagedRecords);
|
||||
return page;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<OperationAuditLogEntity> findByLogId(String logId) {
|
||||
return Optional.ofNullable(store.get(logId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(OperationAuditLogEntity entity) {
|
||||
store.put(entity.getLogId(), entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<List<OperationAuditLogEntity>> findByAuditStatus(String auditStatus) {
|
||||
List<OperationAuditLogEntity> list = store.values().stream()
|
||||
.filter(e -> auditStatus.equals(e.getAuditStatus()))
|
||||
.collect(Collectors.toList());
|
||||
return list.isEmpty() ? Optional.empty() : Optional.of(list);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user