首页 > 其他分享 >Spring Boot 注解拦截器实现审计日志功能

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

时间:2024-09-22 17:23:08浏览次数:8  
标签:拦截器 Spring Boot example org import 日志 com ecommerce

引言

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

本篇文章将深入讲解如何在 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=UTC
    username: root
    password: password
    driver-class-name: com.mysql.cj.jdbc.Driver

  mybatis:
    mapper-locations: classpath:mapper/*.xml
    type-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 {

    @Autowired
    private AuditLogService auditLogService;

    @Autowired
    private 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();

            // 获取操作对象的ID
            Object[] 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 {

    @Autowired
    private 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 {

    @Autowired
    private 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 {

    @Autowired
    private AuditLogService auditLogService;

    @Autowired
    private 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();

            // 获取操作对象的ID
            Object[] 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. 总结

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

标签:拦截器,Spring,Boot,example,org,import,日志,com,ecommerce
From: https://blog.csdn.net/weixin_39996520/article/details/142408521

相关文章

  • 基于Spring Boot的疫苗接种系统 计算机专业毕业设计源码32315
    摘 要预防预接种工作实行网络信息化管理,是我国免疫规划工作发展的需要。接种信息实行网络信息化不仅是预防接种工作步入了一个新的台阶,更重要的是解决了多年疫苗接种过程种,免疫接种剂次不清,难以全程有效接种的问题;同时各级政府卫生行政部门亦能通过平台可以及时了解本地区免......
  • Spring 的循环依赖
    在Spring中,循环依赖是指两个或多个Bean相互依赖,导致在创建过程中出现了依赖死锁的问题。为了解决循环依赖,Spring引入了三级缓存机制。了解为什么需要三级缓存机制,首先要明白循环依赖是如何发生的,以及两级缓存为什么不足够。一、循环依赖是什么?假设有两个BeanA和B:A......
  • Shiro漏洞复现-springboot运行报错的解决 && 项目本地tomcat部署
    一、Springboot项目运行出现的种种问题:首先需要下载环境代码,来源Github等开源平台,下载解压后使用IDEA打开,由于Shiro通常与Springboot项目捆绑,所以通常运行需要Springboot环境,如下图所示:运行时可能会出现如下报错:unabletostartServletWebServerApplicationcontextd......
  • 蜗牛兼职网:Spring Boot框架应用
    摘要随着科学技术的飞速发展,社会的方方面面、各行各业都在努力与现代的先进技术接轨,通过科技手段来提高自身的优势,蜗牛兼职网当然也不能排除在外。蜗牛兼职网是以实际运用为开发背景,运用软件工程原理和开发方法,采用springboot框架构建的一个管理系统。整个开发过程首先对......
  • 【计算机专业毕设最新Java必过毕设选题2025】基于springboot的高校跳蚤市场管理系统(源
    作品简介 Hi,各位同学好呀!今天向大家分享一个最新完成的高质量毕业设计项目作品基于springboot的XXX管理系统项目评分(最低0分,满分5分)难度系数:3分工作量:5分创新点:3分界面美化:5分使用技术前端:html/js/css后端:springboot数据库:MySql服务器:apache-tomcat......
  • 蜗牛兼职网开发:Spring Boot应用
    第3章系统分析3.1需求分析蜗牛兼职网主要是为了提高工作人员的工作效率和更方便快捷的满足用户和企业,更好存储所有数据信息及快速方便的检索功能,对系统的各个模块是通过许多今天的发达系统做出合理的分析来确定考虑用户和企业的可操作性,遵循开发的系统优化的原则,经过全......
  • spring boot 项目中集成使用 Elasticsearch
    目录前言一、添加依赖二、配置Elasticsearch三、定义实体和仓库四、使用Elasticsearch五、性能和安全优化六、监控和日志七、插件和扩展总结前言在SpringBoot项目中使用Elasticsearch,可以充分发挥Elasticsearch在全文搜索、日志分析、数据索引等方面的......
  • [Spring]过滤器
    过滤器Filter作为Java三大器之一,在JavaWeb的使用中有很高的地位。所谓过滤器,就是实现了javax.servlet.Filter接口的服务器端程序。Filter有如下几个用处:在HttpServletRequest到达Servlet之前,拦截客户的HttpServletRequest。根据需要检查HttpServletRequest,也可以修改HttpSe......
  • Spring Boot技术栈:蜗牛兼职网案例
    第2章相关技术2.1B/S架构B/S结构的特点也非常多,例如在很多浏览器中都可以做出信号请求。并且可以适当的减轻用户的工作量,通过对客户端安装或者是配置少量的运行软件就能够逐步减少用户的工作量,这些功能的操作主要是由服务器来进行控制的,由于该软件的技术不断成熟,最主要的......
  • [Spring]拦截器
    Interceptor介绍拦截器(Interceptor)同Filter过滤器一样,它俩都是面向切面编程——AOP的具体实现(AOP切面编程只是一种编程思想而已)。你可以使用Interceptor来执行某些任务,例如在Controller处理请求之前编写日志,添加或更新配置......在Spring中,当请求发送到Controller......