企业级系统操作审计方案设计
企业级系统操作审计方案设计
引言
操作审计是企业级系统的合规性要求和安全保障手段。完整的审计体系能够记录"谁在什么时间对什么数据做了什么操作",为问题追溯、安全审计和合规检查提供数据支撑。在金融、医疗、政务等受监管行业,操作审计更是法律强制要求。
本文将介绍操作日志 @MyLog 注解设计、登录/登出日志、异常日志自动采集、日志统计分析及数据清理策略的完整方案。
核心内容
审计日志体系全景
graph TD
A[审计日志体系] --> B[操作日志]
A --> C[登录日志]
A --> D[异常日志]
B --> B1[增删改操作]
B --> B2[导出操作]
B --> B3[审批操作]
B --> B4[敏感数据访问]
C --> C1[登录成功]
C --> C2[登录失败]
C --> C3[登出记录]
C --> C4[登录IP/设备]
D --> D1[系统异常]
D --> D2[业务异常]
D --> D3[接口异常]
D --> D4[定时任务异常]
一、操作日志 @MyLog 注解设计
1. 注解定义
/**
* 操作日志注解
* 标注在Controller方法上,自动记录操作日志
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
/** 操作模块 */
String module() default "";
/** 操作类型 */
OperateType type() default OperateType.OTHER;
/** 操作描述 */
String description() default "";
/** 是否保存请求参数 */
boolean saveParams() default true;
/** 是否保存响应结果 */
boolean saveResult() default false;
/** 排除的参数字段(敏感信息脱敏) */
String[] excludeParams() default {"password", "oldPassword", "token"};
}
/** 操作类型枚举 */
public enum OperateType {
CREATE("新增"),
UPDATE("修改"),
DELETE("删除"),
EXPORT("导出"),
IMPORT("导入"),
QUERY("查询"),
APPROVE("审批"),
OTHER("其他");
private final String label;
OperateType(String label) {
this.label = label;
}
public String getLabel() {
return label;
}
}
2. 日志实体
/**
* 操作日志实体
*/
@Data
@TableName("sys_oper_log")
public class OperLog {
/** 日志主键 */
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/** 操作模块 */
private String module;
/** 操作类型 */
private String operateType;
/** 操作描述 */
private String description;
/** 请求方法 */
private String requestMethod;
/** 请求URL */
private String requestUrl;
/** 请求参数 */
private String requestParams;
/** 响应结果 */
private String responseResult;
/** 操作人ID */
private Long operatorId;
/** 操作人姓名 */
private String operatorName;
/** 操作人IP */
private String operatorIp;
/** 操作人地址 */
private String operatorLocation;
/** 执行耗时(毫秒) */
private Long duration;
/** 操作状态:0-成功 1-失败 */
private Integer status;
/** 错误信息 */
private String errorMsg;
/** 操作时间 */
private LocalDateTime operateTime;
}
3. AOP 切面实现
sequenceDiagram
participant 请求
participant Controller
participant MyLogAspect
participant 日志服务
请求->>Controller: 调用方法
Controller->>MyLogAspect: AOP拦截
MyLogAspect->>MyLogAspect: 记录开始时间
MyLogAspect->>Controller: 执行业务方法
Controller-->>MyLogAspect: 返回结果/异常
MyLogAspect->>MyLogAspect: 计算耗时
MyLogAspect->>MyLogAspect: 构建日志对象
MyLogAspect->>日志服务: 异步保存日志
日志服务-->>MyLogAspect: 保存完成
MyLogAspect-->>请求: 返回响应
/**
* 操作日志切面
*/
@Aspect
@Component
@Slf4j
public class MyLogAspect {
@Autowired
private OperLogService operLogService;
/**
* 环绕通知:记录操作日志
*/
@Around("@annotation(myLog)")
public Object around(ProceedingJoinPoint point, MyLog myLog) throws Throwable {
long startTime = System.currentTimeMillis();
OperLog operLog = new OperLog();
try {
// 填充基本信息
fillBaseInfo(operLog, point, myLog);
// 执行业务方法
Object result = point.proceed();
// 记录成功日志
operLog.setStatus(0);
if (myLog.saveResult() && result != null) {
operLog.setResponseResult(truncate(toJson(result), 2000));
}
return result;
} catch (Throwable e) {
// 记录失败日志
operLog.setStatus(1);
operLog.setErrorMsg(truncate(e.getMessage(), 2000));
throw e;
} finally {
// 计算耗时
operLog.setDuration(System.currentTimeMillis() - startTime);
operLog.setOperateTime(LocalDateTime.now());
// 异步保存日志
operLogService.saveAsync(operLog);
}
}
/** 填充基本信息 */
private void fillBaseInfo(OperLog operLog, ProceedingJoinPoint point, MyLog myLog) {
// 注解信息
operLog.setModule(myLog.module());
operLog.setOperateType(myLog.type().name());
operLog.setDescription(myLog.description());
// 请求信息
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
operLog.setRequestMethod(request.getMethod());
operLog.setRequestUrl(request.getRequestURI());
operLog.setOperatorIp(getIpAddress(request));
}
// 操作人信息
if (StpUtil.isLogin()) {
operLog.setOperatorId(StpUtil.getLoginIdAsLong());
operLog.setOperatorName(getCurrentUsername());
}
// 请求参数
if (myLog.saveParams()) {
String params = buildParams(point, myLog.excludeParams());
operLog.setRequestParams(truncate(params, 2000));
}
}
/** 构建请求参数(脱敏处理) */
private String buildParams(ProceedingJoinPoint point, String[] excludeParams) {
Object[] args = point.getArgs();
if (args == null || args.length == 0) {
return "";
}
// 将参数转为Map并脱敏
Map<String, Object> paramsMap = new LinkedHashMap<>();
String[] paramNames = getParameterNames(point);
for (int i = 0; i < args.length; i++) {
String name = (paramNames != null && i < paramNames.length)
? paramNames[i] : "arg" + i;
paramsMap.put(name, args[i]);
}
// 敏感字段脱敏
for (String exclude : excludeParams) {
if (paramsMap.containsKey(exclude)) {
paramsMap.put(exclude, "******");
}
}
return toJson(paramsMap);
}
}
4. 使用示例
@RestController
@RequestMapping("/api/system/user")
@Tag(name = "用户管理")
public class UserController {
@MyLog(module = "用户管理", type = OperateType.CREATE, description = "创建用户")
@PostMapping
public Result<Void> create(@RequestBody @Valid UserCreateDTO dto) {
userService.createUser(dto);
return Result.success();
}
@MyLog(module = "用户管理", type = OperateType.UPDATE, description = "更新用户")
@PutMapping
public Result<Void> update(@RequestBody @Valid UserUpdateDTO dto) {
userService.updateUser(dto);
return Result.success();
}
@MyLog(module = "用户管理", type = OperateType.DELETE, description = "删除用户")
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable Long id) {
userService.deleteUser(id);
return Result.success();
}
@MyLog(module = "用户管理", type = OperateType.EXPORT, description = "导出用户",
saveResult = false, saveParams = false)
@GetMapping("/export")
public void export(HttpServletResponse response) {
userService.exportUsers(response);
}
}
二、登录/登出日志
/**
* 登录日志实体
*/
@Data
@TableName("sys_login_log")
public class LoginLog {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/** 用户名 */
private String username;
/** 登录IP */
private String loginIp;
/** 登录地点 */
private String loginLocation;
/** 浏览器 */
private String browser;
/** 操作系统 */
private String os;
/** 登录状态:0-成功 1-失败 */
private Integer status;
/** 提示消息 */
private String message;
/** 登录时间 */
private LocalDateTime loginTime;
}
/**
* 登录日志记录服务
*/
@Service
public class LoginLogService {
@Autowired
private AsyncLogService asyncLogService;
/**
* 记录登录日志
*/
public void recordLoginLog(String username, boolean success, String message) {
HttpServletRequest request = getCurrentRequest();
LoginLog loginLog = new LoginLog();
loginLog.setUsername(username);
loginLog.setLoginIp(getIpAddress(request));
loginLog.setBrowser(getBrowser(request));
loginLog.setOs(getOs(request));
loginLog.setStatus(success ? 0 : 1);
loginLog.setMessage(message);
loginLog.setLoginTime(LocalDateTime.now());
// 异步保存
asyncLogService.saveLoginLog(loginLog);
}
}
三、异常日志自动采集
graph TD
A[异常发生] --> B{异常类型}
B -->|Controller层| C[@RestControllerAdvice捕获]
B -->|Service层| D[向上传播至Controller]
B -->|定时任务| E[TaskExceptionHandler捕获]
B -->|未捕获| F[全局异常处理器]
C --> G[构建异常日志]
D --> G
E --> G
F --> G
G --> H[异步保存异常日志]
/**
* 全局异常处理器——自动采集异常日志
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@Autowired
private AsyncLogService asyncLogService;
/**
* 业务异常
*/
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e, HttpServletRequest request) {
log.warn("业务异常: {}", e.getMessage());
saveErrorLog(request, e, "BUSINESS");
return Result.fail(e.getCode(), e.getMessage());
}
/**
* 参数校验异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidationException(MethodArgumentNotValidException e,
HttpServletRequest request) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
.collect(Collectors.joining("; "));
saveErrorLog(request, e, "VALIDATION");
return Result.fail(400, message);
}
/**
* 系统异常
*/
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e, HttpServletRequest request) {
log.error("系统异常", e);
saveErrorLog(request, e, "SYSTEM");
return Result.fail(500, "系统异常,请联系管理员");
}
/** 保存异常日志 */
private void saveErrorLog(HttpServletRequest request, Exception e, String errorType) {
ErrorLog errorLog = new ErrorLog();
errorLog.setErrorType(errorType);
errorLog.setRequestUrl(request.getRequestURI());
errorLog.setRequestMethod(request.getMethod());
errorLog.setErrorMessage(truncate(e.getMessage(), 2000));
errorLog.setErrorStack(truncate(getStackTrace(e), 4000));
errorLog.setOperatorIp(getIpAddress(request));
if (StpUtil.isLogin()) {
errorLog.setOperatorId(StpUtil.getLoginIdAsLong());
}
asyncLogService.saveErrorLog(errorLog);
}
}
四、日志统计分析
graph TD
A[日志统计分析] --> B[操作排行]
A --> C[异常排行]
A --> D[用户活跃度]
A --> E[趋势分析]
B --> B1[操作类型分布]
B --> B2[高频操作Top10]
B --> B3[模块操作统计]
C --> C1[异常类型分布]
C --> C2[高频异常Top10]
C --> C3[异常趋势]
D --> D1[日活/周活/月活]
D --> D2[用户操作频次]
D --> D3[在线时长统计]
E --> E1[每日操作量趋势]
E --> E2[登录趋势]
E --> E3[异常趋势]
/**
* 日志统计服务
*/
@Service
public class LogStatisticsService {
/**
* 操作类型分布统计
*/
public List<StatisticsVO> operateTypeDistribution(LocalDate startDate, LocalDate endDate) {
return operLogMapper.selectList(new LambdaQueryWrapper<OperLog>()
.between(OperLog::getOperateTime, startDate.atStartOfDay(),
endDate.atTime(23, 59, 59))
.select(OperLog::getOperateType))
.stream()
.collect(Collectors.groupingBy(OperLog::getOperateType, Collectors.counting()))
.entrySet().stream()
.map(e -> new StatisticsVO(e.getKey(), e.getValue()))
.sorted(Comparator.comparing(StatisticsVO::getCount).reversed())
.toList();
}
/**
* 高频操作Top10
*/
public List<StatisticsVO> topOperations(LocalDate startDate, LocalDate endDate, int limit) {
return operLogMapper.selectTopOperations(startDate, endDate, limit);
}
/**
* 用户活跃度统计
*/
public UserActivityVO userActivity(LocalDate startDate, LocalDate endDate) {
long dau = loginLogMapper.countDistinctUser(startDate, endDate); // 日活
long totalOperations = operLogMapper.countByDateRange(startDate, endDate);
double avgOperations = totalOperations / (double) dau;
return new UserActivityVO(dau, totalOperations, avgOperations);
}
/**
* 每日操作量趋势
*/
public List<TrendVO> operateTrend(LocalDate startDate, LocalDate endDate) {
return operLogMapper.selectDailyTrend(startDate, endDate);
}
}
五、日志数据清理策略
graph TD
A[日志清理策略] --> B[按时间清理]
A --> C[按数量清理]
A --> D[归档后清理]
B --> B1[操作日志保留90天]
B --> B2[登录日志保留180天]
B --> B3[异常日志保留365天]
C --> C1[单表最大100万条]
C --> C2[超出后删除最旧数据]
D --> D1[归档到历史表]
D --> D2[导出为文件]
D --> D3[压缩存储]
style B1 fill:#c8e6c9
style B2 fill:#c8e6c9
style B3 fill:#c8e6c9
/**
* 日志清理定时任务
*/
@Component
@Slf4j
public class LogCleanupTask {
@Autowired
private OperLogMapper operLogMapper;
@Autowired
private LoginLogMapper loginLogMapper;
@Autowired
private ErrorLogMapper errorLogMapper;
/**
* 每天凌晨2点执行日志清理
*/
@Scheduled(cron = "0 0 2 * * ?")
public void cleanupLogs() {
log.info("开始执行日志清理任务...");
// 操作日志保留90天
int operDeleted = operLogMapper.deleteBeforeDate(
LocalDate.now().minusDays(90));
log.info("清理操作日志: {}条", operDeleted);
// 登录日志保留180天
int loginDeleted = loginLogMapper.deleteBeforeDate(
LocalDate.now().minusDays(180));
log.info("清理登录日志: {}条", loginDeleted);
// 异常日志保留365天
int errorDeleted = errorLogMapper.deleteBeforeDate(
LocalDate.now().minusDays(365));
log.info("清理异常日志: {}条", errorDeleted);
log.info("日志清理任务完成");
}
}
六、异步日志保存
/**
* 异步日志保存服务
* 使用独立线程池,避免影响业务性能
*/
@Service
@Slf4j
public class AsyncLogService {
private final ExecutorService logExecutor = Executors.newFixedThreadPool(
2, r -> {
Thread t = new Thread(r, "log-writer");
t.setDaemon(true);
return t;
});
@Autowired
private OperLogMapper operLogMapper;
@Autowired
private LoginLogMapper loginLogMapper;
@Autowired
private ErrorLogMapper errorLogMapper;
/** 异步保存操作日志 */
public void saveOperLog(OperLog operLog) {
logExecutor.execute(() -> {
try {
operLogMapper.insert(operLog);
} catch (Exception e) {
log.error("保存操作日志失败", e);
}
});
}
/** 异步保存登录日志 */
public void saveLoginLog(LoginLog loginLog) {
logExecutor.execute(() -> {
try {
loginLogMapper.insert(loginLog);
} catch (Exception e) {
log.error("保存登录日志失败", e);
}
});
}
/** 异步保存异常日志 */
public void saveErrorLog(ErrorLog errorLog) {
logExecutor.execute(() -> {
try {
errorLogMapper.insert(errorLog);
} catch (Exception e) {
log.error("保存异常日志失败", e);
}
});
}
}
结论与建议
日志保留策略
| 日志类型 | 保留时间 | 单表上限 | 清理方式 |
|---|---|---|---|
| 操作日志 | 90天 | 100万条 | 按时间+按数量 |
| 登录日志 | 180天 | 50万条 | 按时间 |
| 异常日志 | 365天 | 50万条 | 按时间 |
最佳实践建议
-
异步保存:日志保存必须异步执行,避免影响业务接口的响应时间。
-
敏感信息脱敏:请求参数中的密码、Token等敏感信息必须脱敏处理后再存储。
-
内容截断:对请求参数、响应结果、异常堆栈设置最大长度限制,防止超长数据撑爆数据库。
-
按需记录:查询操作默认不记录日志,只记录增删改等变更操作。敏感查询(如查看密码)除外。
-
索引优化:为日志表的
operateTime、operatorId、operateType字段建立索引,加速查询。 -
冷热分离:近期日志存储在MySQL,历史日志归档到Elasticsearch或对象存储,降低数据库压力。