Mybatis Plus轻松实现数据库变更全局审计日志

Mybatis Plus轻松实现数据库变更全局审计日志

  • Mybatis Plus轻松实现数据库变更全局审计日志
    • 引言
    • 实现审计日志
      • 1.创建审计日志表
      • 2.创建AuditLogAspect用于记录请求日志
      • 4. 保存审计日志
    • 总结

Mybatis Plus轻松实现数据库变更全局审计日志

引言

在日常的业务开发中,监控与记录数据库的变化是非常必要的操作,特别是当出现数据异常时,我们可以通过审计日志追溯数据变化的具体情况。Mybatis Plus作为一款优秀的持久层框架,其强大的功能可以轻松帮助我们实现全局审计日志。接下来,我将向大家介绍如何利用Mybatis Plus实现数据库变更的全局审计日志。

实现审计日志

首先,我们这里说的审计日志,主要包括审计需要的四大元素包括操作用户、操作时间、操作类型以及操作前后的数据对比,本文主要基于这个要求进行实现,实现效果如下图所示:

在这里插入图片描述
接下来介绍具体实现步骤:

1.创建审计日志表

首先,我们需要创建一个审计日志数据库表用于存储日志记录,商品表用于测试变更操作。这个表需要包含用户ID、操作时间、请求ID、操作表名称、变更前,变更后,具体变更内容,操作人员等字段。以下是SQL语句:

create table `audit-log`
(
    id             bigint        not null comment '主键' primary key,
    `request_id`    varchar(50)          not null comment '请求ID',
    `data_change` varchar(1000) null comment '变更项',
    `before_value` varchar(1000) null comment '对象变更前json',
    `after_value`  varchar(1000) null comment '对象变更后json',
    `table_name`   varchar(50)   null comment '变更表名',
    `create_time`  datetime      null comment '变更时间',
    `user_id`      bigint        null comment '操作人员'
  --   可以适当冗余相关用户名,机构ID,机构名称等。
)
    charset = utf8mb4;

create table goods
(
    id          bigint         not null
        primary key,
    name        varchar(100)   not null,
    price       decimal(18, 2) not null,
    description varchar(255)   null,
    brand       varchar(100)   not null,
    image_url   varchar(255)   not null,
    create_time datetime       not null,
    update_time datetime       not null
)
    charset = utf8mb4;

2.创建AuditLogAspect用于记录请求日志

这里我们需要创建一个AOP,用于在指定的包或注解进行拦截,执行前和执行后记录相应的数据,用于构造审计日志,如下代码有详细注释:


@Aspect
@Component
public class AuditLogAspect {

    Logger logger = LoggerFactory.getLogger(AuditLogAspect.class);

    @Autowired
    private ThreadAuditService threadAuditService;

    private static void accept(DomainChangeAction change) {
        List<?> oldObject = change.getOldObject();
        if (CollectionUtils.isEmpty(oldObject)) {
            return;
        }
        List<Map<String, Object>> maps = change.getJdbcTemplate().queryForList(change.getQuerySql());
        change.setNewObject(maps);
    }

    /**
     * <p>
     * 业务方法执行前记录
     * </p>
     *
     * @param auditLogTag AuditLogTag
     * @return void
     */
    @Before("@annotation(auditLogTag)")
    public void beforeDataOperate(JoinPoint joinPoint, AuditLogTag auditLogTag) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        AuditLogDTO auditLogDTO = threadAuditService.getAuditLogDTO();
        if (auditLogDTO == null) {
            auditLogDTO = new AuditLogDTO(IdWorker.getTimeId());  // 创建线程DTO,设置惟一请求ID
            threadAuditService.setAuditLogDTO(auditLogDTO); // 放入线程作用域
        }
        // 获取当前线程中审计日志DTO
        String ClassName = methodSignature.getDeclaringTypeName();
        auditLogDTO.setExecuteMethod(ClassName + "#" + joinPoint.getSignature().getName());
        // 如果要全局进行业务审计的话,则切面就不需要对指定的注解进行了,可以根据实体对象上增加注解标记
        auditLogDTO.setModel(auditLogTag.model());
        auditLogDTO.setTag(auditLogTag.model());
        // TODO:  userId可以从上下文中获取,本例没有集成登录,暂无法获取  auditLogDTO.setUserId();

    }

    /**
     * 业务方法执行后记录
     */
    @AfterReturning("@annotation(com.atshuo.audit.aop.dto.AuditLogTag)")
    public void afterDataOperate() {
        try {
            List<DomainChangeAction> domainChanges = threadAuditService.getAuditLogDTO().getDomainChanges();
            if (CollectionUtils.isEmpty(domainChanges)) {
                return;
            }
            domainChanges.forEach(AuditLogAspect::accept);
            this.compareAndTransfer(domainChanges);
        } catch (Exception e) {
            logger.error("获取变更前后内容出错", e);
        }
    }

    /**
     * 对比保存
     */
    public void compareAndTransfer(List<DomainChangeAction> list) {
        List<AuditLog> auditLogs = new ArrayList<>();
        list.forEach(change -> {
            List<?> oldObject = change.getOldObject();
            List<?> newObject = change.getNewObject();
            // 更新前后数据量,无法对应,不做处理,应该属于逻辑删除。
            if (newObject == null) {
                return;
            }
            if (oldObject == null) {
                return;
            }
            if (oldObject.size() != newObject.size()) {
                return;
            }

            for (int i = 0; i < oldObject.size(); i++) {
                try {
                    String oldDataJson = JSON.toJSONString(oldObject.get(i));
                    String newDataJson = JSON.toJSONString(newObject.get(i));
                    String differenceJson = CompareObjUtil.campareJsonObject(oldDataJson, newDataJson);
                    AuditLog auditLog = new AuditLog();
                    auditLog.setDataChange(differenceJson);
                    auditLog.setTransferData(JSON.toJSONString(change.getTransferData()));
                    auditLog.setTableName(change.getTableName());
                    auditLog.setRequestId(threadAuditService.getAuditLogDTO().getRequestId());
                    auditLog.setId(IdWorker.getId());
                    auditLog.setBeforeValue(oldDataJson);
                    auditLog.setNewValue(newDataJson);
                    auditLog.setExecuteMethod(threadAuditService.getAuditLogDTO().getExecuteMethod());
                    auditLogs.add(auditLog);
                } catch (Exception e) {
                    logger.error("解析变更封装审计日志对象时出错", e);
                }
            }
        });
        logger.info("要保存的操作记录数据:{}", JSON.toJSONString(auditLogs));
        // TODO:  可以保存审计日志到数据库, 或者将该审计模块封装成starter,引入使用,并通过feign接口导步保存处理,等,具体根据使用场景进行处理。
        threadAuditService.clear();
    }

}```

### 3.Mybatis拦截器用于拦截更新操作

这里我们需要创建一个全局的监听器,用于监听任何增删改的数据库操作。在Mybatis Plus中,可以通过实现Interceptor接口来实现,具体代码如下:

```java

/**
 * 业务操作 mybatis 拦截器,拦截所有 update操作。
 * 被 @com.atshuo.audit.aop.AuditLogAspect 中切面切到的方法,有更新操作,会被处理,生成审计日志,并记录数据库
 */
@Slf4j
@Component
@Intercepts({@Signature(type = StatementHandler.class, method = "update", args = {Statement.class})})
public class BussinessOperationInterceptor extends AbstractSqlParserHandler implements Interceptor {

    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Autowired
    private ThreadAuditService threadAuditService;

    @Override
    public Object intercept(Invocation invocation) throws Exception {

        // 判断是否需要记录审计日志,如果AOP没有切到的业务,当前线程中就不存在审计日志对象
        if (threadAuditService.getAuditLogDTO() == null) {
            return invocation.proceed();
        }

        Statement statement;
        Object firstArg = invocation.getArgs()[0];
        if (Proxy.isProxyClass(firstArg.getClass())) {
            statement = (Statement) SystemMetaObject.forObject(firstArg).getValue("h.statement");
        } else {
            statement = (Statement) firstArg;
        }

        MetaObject stmtMetaObj = SystemMetaObject.forObject(statement);

        if (stmtMetaObj.hasGetter("delegate")) {
            statement = (Statement) stmtMetaObj.getValue("delegate");
        } else if (stmtMetaObj.hasGetter("stmt.statement")) {
            statement = (Statement) stmtMetaObj.getValue("stmt.statement");
        }

        String originalSql = statement.toString();
        originalSql = originalSql.replaceAll("[\\s]+", StringPool.SPACE);
        int index = indexOfSqlStart(originalSql);
        if (index > 0) {
            originalSql = originalSql.substring(index);
        }

        StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());

        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        this.sqlParser(metaObject);
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");

        if (mappedStatement.getSqlCommandType() != null) {
            try {
                // 获取执行Sql
                String sql = originalSql.replace("where", "WHERE");
                // 使用mybatis-plus 工具解析sql获取表名
                Collection<String> tables = new TableNameParser(sql).tables();
                if (CollectionUtils.isEmpty(tables)) {
                    return invocation.proceed();
                }
                String tableName = tables.iterator().next();
                //更新数据
                if (SqlCommandType.UPDATE.equals(mappedStatement.getSqlCommandType())) {
                    DomainChangeAction change = new DomainChangeAction();
                    change.setTableName(tableName);
                    change.setJdbcTemplate(jdbcTemplate);
                    // 设置sql用于执行完后查询新数据
                    String selectSql = sql.substring(sql.lastIndexOf("WHERE") + 5);
                    // 同表对同条数据操作多次只进行一次对比
                    if (threadAuditService.getAuditLogDTO().getDomainChanges().stream().anyMatch(c -> tableName.equals(c.getTableName())
                            && selectSql.equals(c.getWhereSql()))) {
                        return invocation.proceed();
                    }
                    change.setWhereSql(selectSql);
                    // 获取请求时object
                    Object parameterObject = statementHandler.getParameterHandler().getParameterObject();
                    change.setTransferData(Arrays.asList(parameterObject));
                    String querySql = "select * from " + tableName + " where " + selectSql;
                    change.setQuerySql(querySql);
                    List<Map<String, Object>> maps = jdbcTemplate.queryForList(querySql);
                    change.setOldObject(maps);
                    change.setEntityClass(parameterObject.getClass());
                    threadAuditService.getAuditLogDTO().getDomainChanges().add(change);
                }
            } catch (Exception e) {
                log.error("获取变更前数据时出错。", e);
            }
        }
        return invocation.proceed();
    }

    /**
     * 获取sql语句开头部分
     *
     * @param sql ignore
     * @return ignore
     */
    private int indexOfSqlStart(String sql) {
        String upperCaseSql = sql.toUpperCase();
        Set<Integer> set = new HashSet<>();
        set.add(upperCaseSql.indexOf("SELECT "));
        set.add(upperCaseSql.indexOf("UPDATE "));
        set.add(upperCaseSql.indexOf("INSERT "));
        set.add(upperCaseSql.indexOf("DELETE "));
        set.remove(-1);
        if (CollectionUtils.isEmpty(set)) {
            return -1;
        }
        List<Integer> list = new ArrayList<>(set);
        list.sort(Comparator.naturalOrder());
        return list.get(0);
    }
}

4. 保存审计日志

通过全局操作监听和AOP技术,取得了审计日志所需要的相关内容,然后在AOP的执行完成后增加记录审计日志的相关内容,可以直接调用相关方法,将审计日志保存到数据库,也可以将审计日志放入MQ,然后通过消费消息异步将审计日志入库保存,建议使用MQ异步的方式,这样做尽可能的降低审计日志的记录对业务系统性能的影响。
在这里插入图片描述
需要详细代码,请关注VX公.众.号:“字节跑动”, 发送"审计日志"获取源码工程。

总结

使用Mybatis Plus实现全局的审计日志并不难,本章以更新操作为例,详细说明的实现步骤,有了审计日志,我们就能非常方便地追踪每一条数据的变化过程。希望这篇文章能对您有所帮助。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/358882.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MySQL十部曲之一:MySQL概述及手册说明

文章目录 数据库、数据库管理系统以及SQL之间的关系关系型数据库与非关系型数据库MySQL程序系统变量系统状态变量SQL模式MySQL数据目录手册语法约定 数据库、数据库管理系统以及SQL之间的关系 名称说明数据库&#xff08;Database&#xff09;即存储数据的仓库&#xff0c;其本…

07. STP的基本配置

文章目录 一. 初识STP1.1. STP概述1.2. STP的出现1.3. STP的作用1.4. STP的专业术语1.5. BPDU的报文格式1.6. STP的选择原则&#xff08;1&#xff09;选择根桥网桥原则&#xff08;2&#xff09;选择根端口原则 1.7. 端口状态1.8. STP报文类型1.9. STP的收敛时间 二. 实验专题…

数据结构——并查集

1.并查集的定义 并查集其实也是一种树形结构&#xff0c;在使用中通常用森林的方式来表示 并查集的逻辑结构其实就是集合 并查集一般可以通过双亲写法&#xff08;顺序结构&#xff09;来完成&#xff0c;即通过一个数组存储父亲结点的下标 int s[10005]; int main() {for(…

原来服务器这么有用-使用轻量应用服务器搭建专属自己PDF处理工具

原来服务器这么有用-使用轻量应用服务器搭建专属自己PDF处理工具 1、前言 PDF文件是日常办公中经常使用的一种文档格式。最近&#xff0c;青阳面临一个问题&#xff1a;某公司发送过来的文件需要我们进行印章流程&#xff0c;但由于该公司系统在电子文件加盖电子公章后会自动…

万户 ezOFFICE wpsservlet SQL注入漏洞复现

0x01 产品简介 万户OA ezoffice是万户网络协同办公产品多年来一直将主要精力致力于中高端市场的一款OA协同办公软件产品,统一的基础管理平台,实现用户数据统一管理、权限统一分配、身份统一认证。统一规划门户网站群和协同办公平台,将外网信息维护、客户服务、互动交流和日…

10V单通道负载开关

概述 EM5220是一款单通道负载开关&#xff0c;具有可编程上升时间和集成输出放电控制。该设备包含一个P沟道NOSFET&#xff0c;可以通过输入进行操作电压范围为4.5V至10V。开关由接通和断开低电平逻辑输入控制&#xff0c;其能够与GPIO信号接口。设备的可编程上升时间可以减少…

代码随想录刷题笔记-Day15

1. 完全二叉树的的节点个数 222. 完全二叉树的节点个数https://leetcode.cn/problems/count-complete-tree-nodes/ 给你一棵 完全二叉树 的根节点 root &#xff0c;求出该树的节点个数。 完全二叉树 的定义如下&#xff1a;在完全二叉树中&#xff0c;除了最底层节点可能没…

课时7:shell基础_shell简介

1.3.1 shell简介 学习目标 这一节&#xff0c;我们从 运维、shell语言、小结 三个方面来学习。 运维 简介 运维是什么&#xff1f;所谓的运维&#xff0c;其实就是公司的内部项目当中的一个技术岗位而已&#xff0c;它主要做的是项目的维护性工作。它所涉及的内容范围非常…

Redhat 8.4 一键安装 Oracle 11GR2 单机版

Oracle 一键安装脚本&#xff0c;演示 Redhat 8.4 一键安装 Oracle 11GR2 单机版过程&#xff08;全程无需人工干预&#xff09;&#xff1a;&#xff08;脚本包括 ORALCE PSU/OJVM 等补丁自动安装&#xff09; ⭐️ 脚本下载地址&#xff1a;Shell脚本安装Oracle数据库 脚本…

插接母线温度在线监测系统研究与应用

摘要&#xff1a;低压封闭式插接母线是供配电设施的关键部件&#xff0c;安装在生产车间内部高空&#xff0c;不易保养和维护&#xff0c;在安装不良或保养不当时易发生故障。插接点温度的异常变化与母线故障的发生有着密切的关系&#xff0c;以汽车整车制造工厂为例&#xff0…

Unity 策略模式(实例详解)

文章目录 简介示例1&#xff1a;角色攻击行为示例2&#xff1a;游戏内购折扣策略示例3&#xff1a;NPC寻路策略示例4&#xff1a;动画过渡策略示例5&#xff1a;敌人AI决策策略 简介 在Unity中使用策略模式&#xff0c;我们可以将不同的行为或算法封装成独立的类&#xff08;策…

SpringMVC 自动配置

SpringMVC 自动配置 一、WebMvcAutoConfiguration&#xff08;SpringMVC自动配置&#xff09;二、DisPatcherServletAutoConfiguration.class&#xff08;中央调度器自动配置&#xff09;三、WebMvcConfigurationSupport&#xff08;SpringMVC组件配置类&#xff09;四、Servle…

iOS 17.4 苹果公司正在加倍投入人工智能

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

网络隔离场景下访问 Pod 网络

接着上文 VPC网络架构下的网络上数据采集 介绍 考虑一个监控系统&#xff0c;它的数据采集 Agent 是以 daemonset 形式运行在物理机上的&#xff0c;它需要采集 Pod 的各种监控信息。现在很流行的一个监控信息是通过 Prometheus 提供指标信息。 一般来说&#xff0c;daemonset …

低功耗蓝牙(BLE) 和 经典蓝牙(SPP) 的区别

低功耗蓝牙(BLE) vs 经典蓝牙(SPP) 区别项低功耗蓝牙(BLE)经典蓝牙(SPP 串行端口协议)蓝牙版本蓝牙版本 > 4.0&#xff0c;又称蓝牙低功耗、蓝牙智能经典蓝牙2.0 或更早版本&#xff0c;经典配对模式在两台蓝牙设备之间建立虚拟串口数据连接&#xff0c;提供一种简单而直接…

06.VLAN、Trunk和Hybrid配置

文章目录 一. 初识VLAN1.1. VLAN概述1.2. VLAN的优势1.3. VLAN的帧格式1.4. 接口链路类型1.5. 默认VLAN1.6. VLAN划分方式 二. 实验题2.1. 实验1&#xff1a;划分VLAN2.1.1. 实验目的2.1.2. 实验拓扑图2.1.3. 实验步骤&#xff08;1&#xff09;配置PC机的IP地址&#xff08;2&…

stable diffusion学习笔记——文生图(二)

LORA和Embeddings都可以对画面内容进行调整。目前LORA主要用来定义画面特征&#xff0c;如具体的人物&#xff0c;衣物&#xff0c;画风等。Embeddings目前主要用于反面提示词中&#xff0c;用来避免错误的画面表现。 LORA lora的全称为&#xff1a;低秩适应模型。lora的基本…

05. 交换机的基本配置

文章目录 一. 初识交换机1.1. 交换机的概述1.2. Ethernet_ll格式1.3. MAC分类1.4. 冲突域1.5. 广播域1.6. 交换机的原理1.7. 交换机的3种转发行为 二. 初识ARP2.1. ARP概述2.2. ARP报文格式2.3. ARP的分类2.4. 免费ARP的作用 三. 实验专题3.1. 实验1&#xff1a;交换机的基本原…

常用芯片学习——AMS1117芯片

AMS1117 1A 低压差线性稳压器 使用说明 AMS1117 是一款低压差线性稳压电路&#xff0c;该电路输出电流能力为1A。该系列电路包含固定输出电压版本和可调输出电压版本&#xff0c;其输出电压精度为士1.5%。为了保证芯片和电源系统的稳定性&#xff0c;XBLWAMS1117 内置热保护和…

Nulls: Nothing to Worry About

本文是文章Nulls: Nothing to Worry About的翻译笔记。 避免三值逻辑出现问题。 ISO SQL 标准中的NULL可以是任何东西&#xff0c;但不是一个值。 NULL是指示完全缺乏值的标记。 它们会导致三值逻辑&#xff0c;使用起来很混乱&#xff0c;而且这种混乱常常导致粗心的人编写返…
最新文章