maxwell's + mysql + springboot + mybatisplus (部分技术) 打造业务数据级审计日志

737 阅读4分钟

文章内容作者实现了, 换了个思路

文章内容作者实现了, 换了个思路

业务日志

简单业务

简单业务日志只需要自己手撸一个aop切面,通过织入点实现一些功能就可以实现 较为简单

简单业务

数据审计通常用作"审计系统" 的一部分进行开发例如 用户行为审计 流量审计....

简单业务 + 简单业务

实现样例:

用户模块时间数据详情
张三目录2020.2.2 20:20:20{目录原来名称: "ssss",目录名称:"dddd",xxxxxxx}张三在2020.2.2 20:20:20 修改了目录;修改内容:{目录原来名称: "ssss",目录名称:"dddd",xxxxxxx}

张三在2020.2.2 20:20:20 修改了目录;修改内容:{目录原来名称: "ssss",目录名称:"dddd",xxxxxxx}

tmd 这个问题老子也不知道那个鬼才想起来的, 目前构想为

  • 通过aop自定义业务日志
  • 通过maxwell's 实现数据日志
  • 两个结合 实现业务数据日志

感谢大佬文章帮我打开思路:点我

实现思路

  1. 获取业务日志内容
  2. 获取当前sqlsessionID
  3. 根据maxwell's中 xid 做匹配拼接数据
  4. 根据实体类swagger 说明获取中文对应名称

目前实现

    1. 自定义starter-logger 依赖提供外部支持, 提升复用和减少耦合 (项目要求)
    • 1.1. 定义kafka 主题并在starter中实现生产消息
    • 1.2. starter 中通过上下文获取当前sqlSessionFactory 并拿到xid
      • 1.2.1 上图 不知道这个是不是 没有进行测试呢 image-20210517102740422.png
    1. 项目需要完善实体类swagger 信息

未完待续

如大佬们有别的已经实现或者更好的方案 可以联系我 非常感性

如对代码或者流程存在疑问,请留言 看到必回.

最新实现思路

MD 流程图丢了 大家对付看吧

用到主要技术 (由原有sqlID思想改为吸血鬼思想改为)

  • kafka
  • mybatisplus

主要思路 由于上文中 maxwell's 的必要技术条件(jdk版本问题) 和其他原因(无法获取sqlId)导致此功能无法实现, 转变思路为:

  1. 业务端(需要将操作或数据进行采集记录的服务/应用) 不改动任何东西
    1. 新建starter 自封装pom 内集成mybatisplus(由于理论上此组件引用没有业务端引用级别高所以需要注意版本问题),
    2. 集成kafka (同样注意版本控制问题),
    3. 内实现aop切面提供注解通过织入点(一般是环绕增强)获取方法内入参记以及出参,
    4. 在进入业务方法前通过获取类中this指向的mybatis提供的基础service类获取通用 select等查询方法,
    5. 在执行业务前先调用一次记录数据(利用invoke), 在业务方法执行后在调用一次 上图

image.png

主要切面代码

@Slf4j @Aspect public class OperateRecordAspect {

// kafka
@Resource
private KafkaConfigService kafkaConfigService;

// kafka
@Resource
private KafkaProducerService kafkaProducerService;

@Value("${logger.appName}")
private String appName;


/**
 * .*.* 是去除项目中关键字
 */
@Pointcut("@annotation(com.*.*.aop.OperateRecord)")
public void OperateRecordService() {
}


@Around(value = "OperateRecordService()")
public Object operateRecordServiceAround(ProceedingJoinPoint joinPoint) throws Throwable {
    Object result = ExceptionEnum.LOGIC_SUCCESS;
    try {
        // 拿到请求头 获取session/jwt/oauth 认证token
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        //获取方法
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        OperateRecord operateRecord = method.getAnnotation(OperateRecord.class);

        // 获取类,并处理类名称
        Class<?> clazz = Class.forName(methodSignature.getDeclaringType().getName());
        String[] className = clazz.getName().split("\.");
        
        //这步很关键 不然你执行invoke时报错, 因为invoke执行时不包含上下文信息的, 也就说你调不到当前环境的方法
        ApplicationContext applicationContext = SpringBootBeanUtil.getApplicationContext();

        LoggerModel loggerModel = new LoggerModel();
        // 获取当前是不是读操作 / 增删==写  查询==读 
        String runMed = operateRecord.query();
        if (operateRecord.run()) {
            // 获取主键  1.表达式获取 2.SpEL表达式 
            // 这里主要是获取对象中的id 用于快速查询操作数据
            String biz;
            if (StringUtils.hasText(operateRecord.businessKey()))
                biz = String.valueOf(AspectSupportUtils.getKeyValue(joinPoint, operateRecord.businessKey()));
            else
                biz = String.valueOf(CustomExpressionEvaluator.operateEvaluatorGetBiz(joinPoint, "id"));
            // 
            for (Method mm : clazz.getMethods()) {
                if (mm.getName().equals(runMed)) {
                    // 更新
                    if (operateRecord.operateType().equals(OperateTypeEnum.UPDATE)) {
                    更新会执行两次invoke
                        loggerModel.setOldData(mm.invoke(applicationContext.getBean(CustomExpressionEvaluator.toLowerCaseFirstOne(className[className.length - 1])), biz));
                        result = joinPoint.proceed();
                        loggerModel.setNewData(mm.invoke(applicationContext.getBean(CustomExpressionEvaluator.toLowerCaseFirstOne(className[className.length - 1])), biz));
                    } else if (
                    查询后查询是为了获取id (其实insert后mybatis会根据将值映射会对象属性上,但是这个项目代码很乱,不敢保证所以特意又查了一下)
                    operateRecord.operateType().equals(OperateTypeEnum.INSERT)) {
                        result = joinPoint.proceed();
                        loggerModel.setNewData(mm.invoke(applicationContext.getBean(CustomExpressionEvaluator.toLowerCaseFirstOne(className[className.length - 1])), result));
                    } else if (operateRecord.operateType().equals(OperateTypeEnum.DELETE)) {
                        loggerModel.setOldData(mm.invoke(applicationContext.getBean(CustomExpressionEvaluator.toLowerCaseFirstOne(className[className.length - 1])), biz));
                        result = joinPoint.proceed();
                    } else {
                        log.warn("The operation type is query, and this data has not been recorded!");
                        result = joinPoint.proceed();
                    }
                    break; // 结束循环
                }
            }
        }else
            result = joinPoint.proceed();

        Class<?> klass = operateRecord.entityClazz();
        if (klass != Object.class) {
            Field[] fields = klass.getDeclaredFields();
            Map<String, DescModel> descMap = new HashMap();
            ModelDesc desc;
            for (Field field : fields) {
                desc = field.getAnnotation(ModelDesc.class);
                if (null != desc) {
                    descMap.put(field.getName(), new DescModel(desc.value(), desc.statusDesc(), desc.flag()));
                }
            }
            loggerModel.setModel(descMap);
        }else
            loggerModel.setModel(new HashMap());

        loggerModel.setUserId(request.getHeader("user"));

        loggerModel.setIp(IpUtil.getIpAddr(request));

        loggerModel.setDate(new Date());

        loggerModel.setOperateType(operateRecord.operateType());
        if (operateRecord.paramsRecords())
            loggerModel.setMethodParms(Arrays.toString(joinPoint.getArgs()));

        loggerModel.setMethodName(operateRecord.methodName());

        loggerModel.setAppName(this.appName);
//            map.put("appName", );
        loggerModel.setMethodDescribe(CustomExpressionEvaluator.operateEvaluator(operateRecord.operateDesc(), joinPoint, result));
//            kafkaProducerService.send(kafkaConfigService.getTopic(), JSONObject.toJSONString(loggerModel), true);
        kafkaProducerService.sendMsg(kafkaConfigService.getTopic(),loggerModel);
        return result;
    } catch (Throwable ex) {
        ex.printStackTrace();
        return joinPoint.proceed();
    }
}


@AfterThrowing(value = "OperateRecordService()",throwing="error")
public void operateRecordServiceAround(JoinPoint joinPoint, Throwable error) throws Throwable {
    log.error(error.getMessage());
    log.error("Logic have a error,AOP record fail!");
    log.error("Name is {}",joinPoint.getSignature().getName());
}

}