Spring Boot 注解拦截器实现审计日志功能

server/2024/9/22 8:23:32/

引言

在业务系统中,审计日志记录至关重要。系统需要记录用户的操作日志,特别是在用户操作数据库修改、查询、删除重要数据时,系统应追踪操作人的身份、操作的对象、操作的时间等关键数据。这不仅对运维、合规性有帮助,同时也能提高系统的可审计性和安全性。

本篇文章将深入讲解如何在 Spring Boot 中通过注解和拦截器实现审计日志功能。通过自定义注解,可以在不同模块、不同操作上灵活地记录审计信息,包括操作模块、操作对象属性、用户信息和 IP 地址。同时,这一方案具有高度的拓展性,可以适配于不同业务场景。

我们将以电商交易系统为案例进行详细说明,提供表结构设计和完整的代码示例。


1. 项目环境与依赖

在实现审计日志功能之前,我们需要确保项目的环境和依赖配置正确。本例使用的技术栈如下:

  • Spring Boot 2.x
  • Maven
  • JDK 8+
  • MySQL (用于存储审计日志)
  • Lombok (简化 POJO 开发)

1.1 Maven 依赖

首先,在 pom.xml 文件中加入所需的依赖。主要包含 Spring Web、MyBatis 和 Lombok。

<dependencies><!-- Spring Boot Web Starter --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- MyBatis Starter --><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.4</version></dependency><!-- Lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><scope>provided</scope></dependency><!-- MySQL Driver --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><!-- Spring Boot DevTools (for development) --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope></dependency>
</dependencies>

1.2 数据库配置

application.yml 中配置数据库连接信息。

spring:datasource:url: jdbc:mysql://localhost:3306/ecommerce_db?useSSL=false&serverTimezone=UTCusername: rootpassword: passworddriver-class-name: com.mysql.cj.jdbc.Drivermybatis:mapper-locations: classpath:mapper/*.xmltype-aliases-package: com.example.ecommerce.model

2. 数据库表结构设计

为了记录审计日志,我们需要设计一个用于存储日志信息的数据库表。这里,我们设计一个 audit_logs 表,用于保存操作模块、操作的对象信息、操作用户、IP 地址等审计数据。

2.1 审计日志表 audit_logs

CREATE TABLE `audit_logs` (`id` BIGINT PRIMARY KEY AUTO_INCREMENT,`module_name` VARCHAR(255) NOT NULL, -- 操作模块`object_id` VARCHAR(255) NOT NULL, -- 操作对象的ID(例如订单ID、用户ID等)`object_detail` TEXT, -- 操作对象的详细信息(可选)`operation` VARCHAR(255) NOT NULL, -- 操作类型,如创建、修改、删除`user_id` BIGINT NOT NULL, -- 操作用户的ID`username` VARCHAR(255) NOT NULL, -- 操作用户的名称`ip_address` VARCHAR(50), -- 用户的IP地址`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- 记录时间
);

字段解释:

  • module_name:记录操作发生在哪个模块,比如“订单模块”或“用户模块”。
  • object_id:记录操作对象的主键 ID,如修改的是订单,记录订单 ID。
  • object_detail:操作对象的详细信息,如订单的具体信息,方便后续审计。
  • operation:记录用户的操作类型,如创建、修改、删除等。
  • user_idusername:操作用户的信息。
  • ip_address:用户操作时的 IP 地址。
  • created_at:记录审计日志创建的时间。

3. 自定义注解 @AuditLog

3.1 注解设计

通过自定义注解 @AuditLog,我们可以标记在需要记录日志的地方,比如在 Service 层或 Controller 层。注解的参数可以包括操作模块名、需要记录的对象属性等。

package com.example.ecommerce.annotation;import java.lang.annotation.*;/*** 用于记录操作审计日志的自定义注解*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuditLog {/*** 操作模块名称(如 "订单模块", "用户模块")*/String moduleName();/*** 操作类型(如 "创建", "修改", "删除")*/String operation();/*** 指定操作对象的属性(如 "orderId" 或 "userId")*/String objectId() default "id";
}
  • moduleName:指定操作的模块名,便于区分日志来源。
  • operation:操作类型,如创建、修改、删除等。
  • objectId:用于标识操作对象的主键属性。

4. 实现审计日志拦截器

4.1 用户上下文 UserContext

首先我们创建一个用户上下文 UserContext,用来保存当前用户的登录信息和 IP 地址。在实际应用中,用户登录信息一般是通过 JWT 或 Session 获取的,这里为了简化,假设这些信息已经存在。

package com.example.ecommerce.util;public class UserContext {private static final ThreadLocal<Long> userId = new ThreadLocal<>();private static final ThreadLocal<String> username = new ThreadLocal<>();private static final ThreadLocal<String> ipAddress = new ThreadLocal<>();public static void setUserId(Long id) {userId.set(id);}public static Long getUserId() {return userId.get();}public static void setUsername(String name) {username.set(name);}public static String getUsername() {return username.get();}public static void setIpAddress(String ip) {ipAddress.set(ip);}public static String getIpAddress() {return ipAddress.get();}public static void clear() {userId.remove();username.remove();ipAddress.remove();}
}

4.2 审计日志拦截器

接下来,我们实现一个 Spring 的 HandlerInterceptor 拦截器,用于拦截带有 @AuditLog 注解的方法,并记录日志。

package com.example.ecommerce.interceptor;import com.example.ecommerce.annotation.AuditLog;
import com.example.ecommerce.model.AuditLogRecord;
import com.example.ecommerce.service.AuditLogService;
import com.example.ecommerce.util.UserContext;
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 javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;/*** 用于记录审计日志的拦截器*/
@Aspect
@Component
public class AuditLogInterceptor {@Autowiredprivate AuditLogService auditLogService;@Autowiredprivate HttpServletRequest request;@Around("@annotation(com.example.ecommerce.annotation.AuditLog)")public Object logAudit(ProceedingJoinPoint joinPoint) throws Throwable {Method method = getTargetMethod(joinPoint);if (method == null) {return joinPoint.proceed();}AuditLog auditLog = method.getAnnotation(AuditLog.class);if (auditLog != null) {// 获取用户信息和 IP 地址Long userId = UserContext.getUserId();String username = UserContext.getUsername();String ipAddress = request.getRemoteAddr();// 获取操作对象的IDObject[] args = joinPoint.getArgs();String objectId = getObjectId(args, auditLog.objectId());// 执行目标方法Object result = joinPoint.proceed();// 创建日志记录AuditLogRecord record = new AuditLogRecord();record.setModuleName(auditLog.moduleName());record.setOperation(auditLog.operation());record.setUserId(userId);record.setUsername(username);record.setIpAddress(ipAddress);record.setObjectId(objectId);// 保存日志auditLogService.saveLog(record);return result;}return joinPoint.proceed();}private Method getTargetMethod(ProceedingJoinPoint joinPoint) {Method method = null;try {method = joinPoint.getTarget().getClass().getMethod(joinPoint.getSignature().getName(),((MethodSignature) joinPoint.getSignature()).getParameterTypes());} catch (NoSuchMethodException e) {e.printStackTrace();}return method;}private String getObjectId(Object[] args, String objectIdField) {try {for (Object arg : args) {Field field = arg.getClass().getDeclaredField(objectIdField);field.setAccessible(true);return String.valueOf(field.get(arg));}} catch (Exception e) {// log the error}return null;}
}

4.3 日志服务

我们需要提供一个 AuditLogService,用来保存日志信息。

package com.example.ecommerce.service;import com.example.ecommerce.model.AuditLogRecord;
import com.example.ecommerce.mapper.AuditLogMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;@Service
public class AuditLogService {@Autowiredprivate AuditLogMapper auditLogMapper;public void saveLog(AuditLogRecord record) {auditLogMapper.insert(record);}
}

4.4 日志记录实体

package com.example.ecommerce.model;import lombok.Data;@Data
public class AuditLogRecord {private Long id;private String moduleName;private String operation;private String objectId;private String objectDetail;private Long userId;private String username;private String ipAddress;private String createdAt;
}

4.5 Mapper 定义

package com.example.ecommerce.mapper;import com.example.ecommerce.model.AuditLogRecord;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;@Mapper
public interface AuditLogMapper {@Insert("INSERT INTO audit_logs(module_name, operation, object_id, user_id, username, ip_address, created_at) " +"VALUES (#{moduleName}, #{operation}, #{objectId}, #{userId}, #{username}, #{ipAddress}, NOW())")void insert(AuditLogRecord logRecord);
}

5. 示例使用

OrderService 中,我们可以通过 @AuditLog 注解来记录订单的创建操作。

package com.example.ecommerce.service;import com.example.ecommerce.annotation.AuditLog;
import com.example.ecommerce.model.Order;
import org.springframework.stereotype.Service;@Service
public class OrderService {@AuditLog(moduleName = "订单模块", operation = "创建订单", objectId = "orderId")public void createOrder(Order order) {// 订单创建逻辑}
}

在执行 createOrder 方法时,日志将自动记录到 audit_logs 表中。


6. 注解拦截器实现异步审计日志功能

通过自定义注解 @AuditLog 结合拦截器,实现了审计日志功能,记录用户的操作日志。然而,审计日志功能属于辅助功能,它的执行不应该影响到主流程的性能,尤其是在高并发的场景中,日志记录操作可能会成为性能瓶颈。
进一步优化审计日志的实现,将日志记录功能改为异步处理,从而提高接口的性能和响应速度。


6.1 异步处理的必要性

在实际场景中,审计日志功能仅用于记录用户的操作行为,这类操作通常是写入数据库或记录到日志系统中。虽然日志写入过程本身并不复杂,但如果将日志写入与主业务逻辑串行执行,可能会增加响应时间,特别是在高并发场景下。

通过异步化处理,我们可以将日志的记录放到后台线程中执行,主业务流程无需等待日志记录完成,从而提升接口的性能。


6.2 启用异步支持

首先,在 Spring Boot 的主类上添加 @EnableAsync 注解,启用异步功能。

package com.example.ecommerce;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;@SpringBootApplication
@EnableAsync
public class EcommerceApplication {public static void main(String[] args) {SpringApplication.run(EcommerceApplication.class, args);}
}

6.3 配置线程池

为了更好地处理异步任务,我们可以自定义一个线程池用于执行异步任务。通过线程池可以更好地控制并发数量以及任务的执行速度。

config 包下创建一个 AsyncConfig 类来配置线程池。

package com.example.ecommerce.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;import java.util.concurrent.Executor;@Configuration
@EnableAsync
public class AsyncConfig {@Bean(name = "auditLogExecutor")public Executor asyncExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(5); // 核心线程数executor.setMaxPoolSize(10); // 最大线程数executor.setQueueCapacity(500); // 队列容量executor.setThreadNamePrefix("AuditLog-"); // 线程名称前缀executor.initialize();return executor;}
}

在上述配置中,ThreadPoolTaskExecutor 用于处理异步任务,auditLogExecutor 线程池负责异步执行日志记录任务。配置中我们设置了核心线程数为 5,最大线程数为 10,队列容量为 500。可以根据实际需求调整这些参数。


6.4 修改日志服务

接下来,我们将之前的 AuditLogService 进行修改,使其能够异步记录日志。只需要在日志保存方法上加上 @Async 注解,并指定执行的线程池。

package com.example.ecommerce.service;import com.example.ecommerce.model.AuditLogRecord;
import com.example.ecommerce.mapper.AuditLogMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;@Service
public class AuditLogService {@Autowiredprivate AuditLogMapper auditLogMapper;/*** 异步保存审计日志*/@Async("auditLogExecutor")public void saveLog(AuditLogRecord record) {// 模拟一个较为耗时的日志记录操作try {Thread.sleep(200); // 模拟耗时操作} catch (InterruptedException e) {e.printStackTrace();}// 保存日志记录到数据库auditLogMapper.insert(record);}
}

saveLog 方法上,添加了 @Async("auditLogExecutor") 注解,表示该方法会在我们之前配置的 auditLogExecutor 线程池中异步执行。当该方法被调用时,Spring 会将其丢到异步线程中执行,而不会阻塞主线程。

6.5 审计日志拦截器保持不变

我们之前的审计日志拦截器实现并不需要修改,拦截器依旧会在标记有 @AuditLog 注解的方法执行前后进行日志记录操作。唯一的不同是 AuditLogService.saveLog 现在是异步执行的,因此不会阻塞业务方法的执行。

package com.example.ecommerce.interceptor;import com.example.ecommerce.annotation.AuditLog;
import com.example.ecommerce.model.AuditLogRecord;
import com.example.ecommerce.service.AuditLogService;
import com.example.ecommerce.util.UserContext;
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 javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;@Aspect
@Component
public class AuditLogInterceptor {@Autowiredprivate AuditLogService auditLogService;@Autowiredprivate HttpServletRequest request;@Around("@annotation(com.example.ecommerce.annotation.AuditLog)")public Object logAudit(ProceedingJoinPoint joinPoint) throws Throwable {Method method = getTargetMethod(joinPoint);if (method == null) {return joinPoint.proceed();}AuditLog auditLog = method.getAnnotation(AuditLog.class);if (auditLog != null) {// 获取用户信息和 IP 地址Long userId = UserContext.getUserId();String username = UserContext.getUsername();String ipAddress = request.getRemoteAddr();// 获取操作对象的IDObject[] args = joinPoint.getArgs();String objectId = getObjectId(args, auditLog.objectId());// 执行目标方法Object result = joinPoint.proceed();// 创建日志记录AuditLogRecord record = new AuditLogRecord();record.setModuleName(auditLog.moduleName());record.setOperation(auditLog.operation());record.setUserId(userId);record.setUsername(username);record.setIpAddress(ipAddress);record.setObjectId(objectId);// 异步保存日志auditLogService.saveLog(record);return result;}return joinPoint.proceed();}private Method getTargetMethod(ProceedingJoinPoint joinPoint) {Method method = null;try {method = joinPoint.getTarget().getClass().getMethod(joinPoint.getSignature().getName(),((MethodSignature) joinPoint.getSignature()).getParameterTypes());} catch (NoSuchMethodException e) {e.printStackTrace();}return method;}private String getObjectId(Object[] args, String objectIdField) {try {for (Object arg : args) {Field field = arg.getClass().getDeclaredField(objectIdField);field.setAccessible(true);return String.valueOf(field.get(arg));}} catch (Exception e) {// log the error}return null;}
}

6.6 示例使用

假设我们有一个订单模块的 OrderService,通过 @AuditLog 注解,我们可以记录订单创建的操作。由于审计日志的记录现在是异步进行的,因此不会影响接口的响应性能。

package com.example.ecommerce.service;import com.example.ecommerce.annotation.AuditLog;
import com.example.ecommerce.model.Order;
import org.springframework.stereotype.Service;@Service
public class OrderService {@AuditLog(moduleName = "订单模块", operation = "创建订单", objectId = "orderId")public void createOrder(Order order) {// 订单创建逻辑}
}

createOrder 方法执行时,日志记录的操作会被异步提交给后台线程处理,从而确保主业务的执行不受影响。即便日志记录出现一些延迟,也不会影响主流程的性能。


6.7 日志输出

假设 OrderService.createOrder() 方法被调用,并且当前用户的 ID 为 1,用户名为 john_doe,操作的 IP 地址为 192.168.1.1,记录的审计日志最终会存储在数据库的 audit_logs 表中。

日志记录的 SQL 如下:

INSERT INTO audit_logs (module_name, operation, object_id, user_id, username, ip_address, created_at)
VALUES ('订单模块', '创建订单', '12345', 1, 'john_doe', '192.168.1.1', NOW());

7. 总结

通过自定义注解和拦截器,我们可以轻松实现审计日志的自动化记录。通过该方案,系统不仅可以动态记录用户的操作,还可以灵活地扩展到不同的模块和业务场景。
通过将审计日志的记录改为异步执行,整个系统的性能得到了显著提升。主流程执行完毕后,无需等待日志写入的


http://www.ppmy.cn/server/120193.html

相关文章

Spring框架基础知识

Spring框架基础知识笔记 Spring中bean的生命周期&#xff0c;指Bean从创建、初始化、使用到销毁的整个过程。 Bean实例化->依赖注入->Aware接口的回调->BeanPostProcessor前置处理->初始化方法->BeanPostProcessor后置处理->Bean使用->Bean销毁 循环引用…

物理学基础精解【7】

文章目录 平面方程直角坐标及基本运算 参考文献 平面方程 直角坐标及基本运算 向量的四则运算 下面由文心一言自动生成 向量的四则运算主要包括加法、减法、数乘&#xff08;标量乘法&#xff09;和数量积&#xff08;点积或内积&#xff09;&#xff0c;但通常不直接称为“除…

Elasticsearch如何排序,分页以及高亮查询

目录 一、排序 二、分页查询 三、高亮查询 一、排序 ES中默认使用相关度分数实现排序&#xff0c;可以通过搜索语法定制化排序。 GET /索引/_search { "query": 搜索条件,"sort": [{"字段1":{"order":"asc"} },{ "字…

NumPy库学习之argmax函数

NumPy库学习之argmax函数 一、简介 np.argmax 是 NumPy 库中的一个函数&#xff0c;用于找出数组中最大值的索引。这个函数可以应用于多维数组&#xff0c;并允许沿着指定的轴进行操作&#xff0c;从而返回该轴上最大值的索引。这在处理数据时非常有用&#xff0c;尤其是在需要…

WebServer

一、服务器代码 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <unistd.h> #define PORT 80 #define BUFFER_SIZE 1024 void ha…

2024年华为杯-研赛更新时间轴-资料分享

本次 助攻CDF题 问题一二三问均已完成更新&#xff0c;更新计划轴如图所示 由于赛题之间存在紧密的联系&#xff0c;单独发布问题一二&#xff0c;有可能与明天最终论文不相符&#xff0c;会根据后面问题对前面几问进行调整。个人建议&#xff0c;等明天上午的完整论文即可 题 …

Wpf使用NLog将日志输出到LogViewer

1 LogViewer LogViewer是通过UDP传输的高性能实时log查看器。 具有一下特性&#xff1a; 通过UDP读取日志通过文件导入日志导出日志到一个文件中排序、过滤&#xff08;日志树&#xff0c;日志等级&#xff09;和查找突出显示搜索文本从UPD接收日志时忽略IP地址列表多接收器支…

【nvm管理多版本node】下载安装以及常见问题和解决方案

nvm管理多版本node nvm 下载安装下载安装 nvm 常用命令其他常用命令 常见问题 nvm 下载安装 下载 nvm下载地址 每个版本下都有Assets&#xff0c;根据需要下载一个。 node下载地址 根据自己需要,可以下载可执行文件或者压缩包 安装 按提示安装即可。 安装过程中&#xff…