第一步:构建审计日志存储表
要实现严格的审计,首先需要将所有操作痕迹“落地”。这里我们使用MySQL作为存储介质,设计一张涵盖人员、时间、IP、操作模块、参数及结果的宽表。请直接在数据库中执行以下SQL语句:
```sql
CREATE DATABASE IF NOT EXISTS archive_system DEFAULT CHARSET utf8mb4;
USE archive_system;
CREATE TABLE `sys_audit_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`module` varchar(50) NOT NULL COMMENT '操作模块:如档案管理、用户权限',
`operation` varchar(100) NOT NULL COMMENT '操作名称:如下载档案、删除记录',
`method` varchar(200) DEFAULT NULL COMMENT '执行方法的全路径',
`params` longtext COMMENT '请求参数(JSON格式)',
`ip` varchar(50) DEFAULT NULL COMMENT '请求IP地址',
`username` varchar(50) DEFAULT NULL COMMENT '操作人账号',
`status` tinyint(1) DEFAULT 1 COMMENT '操作状态:1成功,0失败',
`error_msg` varchar(500) DEFAULT NULL COMMENT '失败时的错误信息',
`cost_time` bigint(20) DEFAULT NULL COMMENT '执行耗时(毫秒)',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
PRIMARY KEY (`id`),
KEY `idx_username_time` (`username`, `create_time`),
KEY `idx_module` (`module`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统操作审计日志表';
```
第二步:引入核心依赖与基础配置
基于Spring Boot 2.x环境,我们需要引入AOP(切面编程)来无侵入式地拦截操作,以及JSON处理工具。在你的pom.xml中添加以下依赖:
```xml
org.springframework.boot
spring-boot-starter-aop
org.springframework.boot
spring-boot-starter-web
org.projectlombok
lombok
true
com.fasterxml.jackson.core
jackson-databind
com.baomidou
mybatis-plus-plus-boot-starter
3.5.3.1
mysql
mysql-connector-java
runtime
```
在application.yml中配置数据库连接信息,确保能连接到第一步创建的数据库:
```yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/archive_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
```
第三步:定义审计注解与枚举
为了标记哪些方法需要被审计,我们需要创建一个自定义注解@AuditLog。同时,为了保证数据安全,必须对敏感字段(如身份证号、密码)进行脱敏处理。
创建注解类AuditLog.java:
```java
package com.example.archive.annotation;
import java.lang.annotation.;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuditLog {
String value() default ""; // 操作名称,如 "删除档案"
String module() default ""; // 模块名称,如 "档案管理"
}
```
接着,创建一个简单的工具类DesensitizedUtil.java用于处理敏感数据:
```java
package com.example.archive.util;
import java.util.regex.Pattern;
public class DesensitizedUtil {
// 手机号脱敏
private static final Pattern PHONE_PATTERN = Pattern.compile("(?<=\\d{3})\\d{4}(?=\\d{4})");
// 身份证脱敏
private static final Pattern ID_CARD_PATTERN = Pattern.compile("(?<=\\d{6})\\d{8}(?=\\d{4})");
public static String desensitize(String content) {
if (content == null) return null;
content = PHONE_PATTERN.matcher(content).replaceAll("");
content = ID_CARD_PATTERN.matcher(content).replaceAll("");
return content;
}
}
```
第四步:实现核心AOP切面逻辑(关键)
这是整个系统的核心。切面会拦截带有@AuditLog的方法,提取上下文信息,捕获异常,并记录执行时间。
4.1 获取IP与用户信息工具类
在切面中使用前,先准备好获取真实IP的工具类IpUtils.java:
```java
package com.example.archive.util;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
public class IpUtils {
public static String getIpAddr() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
}
// 模拟获取当前登录用户,实际项目中请从SecurityContext或Token中获取
public static String getCurrentUsername() {
// TODO: 替换为真实的用户获取逻辑
return "admin";
}
}
```
4.2 切面核心代码
创建AuditLogAspect.java:
```java
package com.example.archive.aspect;
import com.example.archive.annotation.AuditLog;
import com.example.archive.entity.SysAuditLog;
import com.example.archive.service.AsyncLogService;
import com.example.archive.util.DesensitizedUtil;
import com.example.archive.util.IpUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Aspect
@Component
public class AuditLogAspect {
@Autowired
private AsyncLogService asyncLogService;
@Autowired
private ObjectMapper objectMapper;
// 定义切点:拦截所有带有 @AuditLog 注解的方法
@Pointcut("@annotation(com.example.archive.annotation.AuditLog)")
public void logPointCut() {}
@Around("logPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
long beginTime = System.currentTimeMillis();
Object result = null;
Exception exception = null;
try {
// 执行业务逻辑
result = point.proceed();
return result;
} catch (Exception e) {
exception = e;
throw e; // 异常必须抛出,否则事务无法回滚
} finally {
long costTime = System.currentTimeMillis() - beginTime;
// 无论成功失败,都记录日志
saveLog(point, costTime, exception);
}
}
private void saveLog(JoinPoint joinPoint, long costTime, Exception exception) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
SysAuditLog log = new SysAuditLog();
// 获取注解上的描述
AuditLog auditLog = method.getAnnotation(AuditLog.class);
if (auditLog != null) {
log.setModule(auditLog.module());
log.setOperation(auditLog.value());
}
// 设置方法路径
String className = joinPoint.getTarget().getClass().getName();
String methodName = signature.getName();
log.setMethod(className + "." + methodName + "()");
// 设置请求参数
try {
Object[] args = joinPoint.getArgs();
String params = objectMapper.writeValueAsString(args);
log.setParams(DesensitizedUtil.desensitize(params)); // 关键:参数脱敏
} catch (Exception e) {
log.setParams("参数解析异常");
}
// 设置基础信息
log.setIp(IpUtils.getIpAddr());
log.setUsername(IpUtils.getCurrentUsername());
log.setCostTime(costTime);
// 设置状态与错误信息
if (exception != null) {
log.setStatus(0);
log.setErrorMsg(exception.getMessage().length() > 500 ? exception.getMessage().substring(0, 500) : exception.getMessage());
} else {
log.setStatus(1);
}
// 异步保存到数据库
asyncLogService.saveSysLog(log);
}
}
```
第五步:异步落库与线程池配置

为了避免审计日志写入数据库影响主业务性能(例如下载大文件时),必须使用异步方式保存日志。首先开启异步支持:
```java
package com.example.archive.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
@Configuration
@EnableAsync
public class AsyncConfig {
// 可以在这里自定义线程池,如不配置则使用Spring默认
}
```
接着创建Service接口和实现类。注意:这里必须使用@Async注解。
```java
package com.example.archive.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.archive.entity.SysAuditLog;
import org.springframework.scheduling.annotation.Async;
public interface AsyncLogService extends IService
{
@Async
void saveSysLog(SysAuditLog log);
}
```
```java
package com.example.archive.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.archive.entity.SysAuditLog;
import com.example.archive.mapper.SysAuditLogMapper;
import com.example.archive.service.AsyncLogService;
import org.springframework.stereotype.Service;
@Service
public class AsyncLogServiceImpl extends ServiceImpl implements AsyncLogService {
@Override
public void saveSysLog(SysAuditLog log) {
// 直接调用MyBatis Plus保存
this.save(log);
}
}
```
对应的实体类SysAuditLog.java(字段需与数据库表对应):
```java
package com.example.archive.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
@Data
@TableName("sys_audit_log")
public class SysAuditLog {
@TableId(type = IdType.AUTO)
private Long id;
private String module;
private String operation;
private String method;
private String params;
private String ip;
private String username;
private Integer status;
private String errorMsg;
private Long costTime;
private Date createTime;
}
```
以及Mapper接口:
```java
package com.example.archive.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.archive.entity.SysAuditLog;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SysAuditLogMapper extends BaseMapper {
}
```
第六步:在业务代码中接入审计
现在,你只需要在需要严格审计的Controller方法上加上@AuditLog注解即可。以下是一个档案管理Controller的示例:
```java
package com.example.archive.controller;
import com.example.archive.annotation.AuditLog;
import org.springframework.web.bind.annotation.;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/archive")
public class ArchiveController {
// 示例:删除档案操作,属于高风险操作,必须审计
@DeleteMapping("/delete/{id}")
@AuditLog(value = "删除档案", module = "档案管理")
public String deleteArchive(@PathVariable Long id) {
// 业务逻辑:删除档案
if (id == 1) {
throw new RuntimeException("模拟删除失败:权限不足");
}
return "删除成功,ID:" + id;
}
// 示例:查询档案详情
@GetMapping("/detail/{id}")
@AuditLog(value = "查看档案详情", module = "档案查询")
public String getDetail(@PathVariable Long id) {
return "档案详情内容...";
}
}
```
第七步:验证审计效果
启动项目后,使用Postman或浏览器访问接口进行测试。
1. 测试正常操作:
访问 GET /archive/detail/100。检查数据库sys_audit_log表,应新增一条记录,status为1,params包含ID信息。
2. 测试异常操作:
访问 DELETE /archive/delete/1。根据代码逻辑,这会抛出异常。检查数据库,status应为0,error_msg应包含“权限不足”。
3. 验证敏感数据脱敏:
如果请求参数中包含手机号或身份证,检查数据库中的params字段,确认中间几位数字已被替换为号。
通过以上步骤,你已经为档案管理系统构建了一套零侵入、高性能且包含数据脱敏的严格审计体系。所有关键操作都有迹可循,满足合规要求。