日志审计设计-结合spring-aop实现

2023-09-20 17:51:13

日志审计设计

设计原则和思路:

元注解方式结合AOP,灵活记录操作日志
能够记录详细错误日志为运营以及审计提供支持
日志记录尽可能减少性能影响
操作描述参数支持动态获取,其他参数自动记录。
1.定义日志记录元注解,

根据业务情况,要求description支持动态入参。例:新增应用{applicationName},其中applicationName是请求参数名。

/**
 * 自定义注解 拦截Controller
 * 
 * @author jianggy
 *
 */
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface SystemControllerLog {
	/**
	 * 描述业务操作 例:Xxx管理-执行Xxx操作
	 * 支持动态入参,例:新增应用{applicationName},其中applicationName是请求参数名
	 * @return
	 */
	String description() default "";
}

2.定义用于记录日志的实体类

package com.guahao.wcp.core.dal.dataobject;

import com.guahao.wcp.core.utils.StringUtils;
import java.io.Serializable;
import java.util.Date;
import java.util.Map;

/**
 * 日志类-记录用户操作行为
 *
 * @author lin.r.x
 */
public class OperateLogDO extends BaseDO implements Serializable {
    private static final long serialVersionUID = -4000845735266995243L;

    private String userId;           //用户ID
    private String userName;         //用户名
    private String desc;            //日志描述
    private int isDeleted;           //状态标识

    private String menuName;         //菜单名称
    private String remoteAddr;       //请求地址
    private String requestUri;       //URI
    private String method;           //请求方式
    private String params;           //提交参数
    private String exception;        //异常信息
    private String type;             //日志类型


    public String getType() {
        return StringUtils.isBlank(type) ? type : type.trim();
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getDesc() {
        return StringUtils.isBlank(desc) ? desc : desc.trim();
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    public String getRemoteAddr() {
        return StringUtils.isBlank(remoteAddr) ? remoteAddr : remoteAddr.trim();
    }

    public void setRemoteAddr(String remoteAddr) {
        this.remoteAddr = remoteAddr;
    }

    public String getRequestUri() {
        return StringUtils.isBlank(requestUri) ? requestUri : requestUri.trim();
    }

    public void setRequestUri(String requestUri) {
        this.requestUri = requestUri;
    }

    public String getMethod() {
        return StringUtils.isBlank(method) ? method : method.trim();
    }

    public void setMethod(String method) {
        this.method = method;
    }

    public String getParams() {
        return StringUtils.isBlank(params) ? params : params.trim();
    }

    public void setParams(String params) {
        this.params = params;
    }

    /**
     * 设置请求参数
     *
     * @param paramMap
     */
    public void setMapToParams(Map<String, String[]> paramMap) {
        if (paramMap == null) {
            return;
        }
        StringBuilder params = new StringBuilder();
        for (Map.Entry<String, String[]> param : ((Map<String, String[]>) paramMap).entrySet()) {
            params.append(("".equals(params.toString()) ? "" : "&") + param.getKey() + "=");
            String paramValue = (param.getValue() != null && param.getValue().length > 0 ? param.getValue()[0] : "");
            params.append(StringUtils.abbr(StringUtils.endsWithIgnoreCase(param.getKey(), "password") ? "" : paramValue, 100));
        }
        this.params = params.toString();
    }

    public String getException() {
        return StringUtils.isBlank(exception) ? exception : exception.trim();
    }

    public void setException(String exception) {
        this.exception = exception;
    }

    public String getUserName() {
        return StringUtils.isBlank(userName) ? userName : userName.trim();
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String getMenuName() {
        return menuName;
    }

    public void setMenuName(String menuName) {
        this.menuName = menuName;
    }

    public int getIsDeleted() {
        return isDeleted;
    }

    public void setIsDeleted(int isDeleted) {
        this.isDeleted = isDeleted;
    }

    @Override
    public String toString() {
        return "OperateLogDO{" +
                "userId='" + userId + '\'' +
                ", userName='" + userName + '\'' +
                ", desc='" + desc + '\'' +
                ", isDeleted=" + isDeleted +
                ", menuName='" + menuName + '\'' +
                ", remoteAddr='" + remoteAddr + '\'' +
                ", requestUri='" + requestUri + '\'' +
                ", method='" + method + '\'' +
                ", params='" + params + '\'' +
                ", exception='" + exception + '\'' +
                ", type='" + type + '\'' +
                '}';
    }
}

3.定义日志AOP切面类,通过logManager.insert(log)往数据库写入日志。

项目pom.xml中增加spring-boot-starter-aop

<dependency>  
            <groupId>org.springframework.boot</groupId>  
            <artifactId>spring-boot-starter-aop</artifactId>  
        </dependency>

具体的日志切点类实现

package com.guahao.wcp.gops.home.aop;

import com.greenline.guser.biz.service.dto.UserInfoDTO;
import com.greenline.guser.client.utils.GuserCookieUtil;
import com.guahao.wcp.gops.home.annotation.SystemControllerLog;
import com.guahao.wcp.gops.home.service.DubboService;
import com.guahao.wcp.core.manager.operatelog.LogManager;
import com.guahao.wcp.core.dal.dataobject.OperateLogDO;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.NamedThreadLocal;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


/**
 * 系统日志切点类
 *
 * @author jianggy
 */
@Aspect
@Component
public class SystemLogAspect {
    private static final Logger logger = LoggerFactory.getLogger(SystemLogAspect.class);
//    private static final ThreadLocal<Date> beginTimeThreadLocal = new NamedThreadLocal<Date>("ThreadLocal beginTime");
    private static final ThreadLocal<OperateLogDO> logThreadLocal = new NamedThreadLocal<OperateLogDO>("ThreadLocal log");
    private static final ThreadLocal<UserInfoDTO> currentUserInfo = new NamedThreadLocal<UserInfoDTO>("ThreadLocal userInfo");

    @Autowired(required = false)
    private HttpServletRequest request;
    @Autowired
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;
    @Autowired
    private LogManager logManager;
    @Autowired
    private DubboService dubboService;

    /**
     * Controller层切点 注解拦截
     */
    @Pointcut("@annotation(com.guahao.wcp.gops.home.annotation.SystemControllerLog)")
    public void controllerAspect() {
    }

    /**
     * 方法规则拦截
     */
    @Pointcut("execution(* com.guahao.wcp.gops.home.controller.*.*(..))")
    public void controllerPointerCut() {
    }

    /**
     * 前置通知 用于拦截Controller层记录用户的操作的开始时间
     *
     * @param joinPoint 切点
     * @throws InterruptedException
     */
    @Before("controllerAspect()")
    public void doBefore(JoinPoint joinPoint) throws InterruptedException {
//        Date beginTime = new Date();
//        beginTimeThreadLocal.set(beginTime);
        //debug模式下 显式打印开始时间用于调试
//        if (logger.isDebugEnabled()) {
//            logger.debug("开始计时: {}  URI: {}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")
//                    .format(beginTime), request.getRequestURI());
//        }
        //读取GuserCookie中的用户信息
        String loginId = GuserCookieUtil.getLoginId(request);
        UserInfoDTO userInfo = dubboService.userInfoService.getUserInfoByLoginId(loginId).getDataResult();
        currentUserInfo.set(userInfo);
    }

    /**
     * 后置通知 用于拦截Controller层记录用户的操作
     *
     * @param joinPoint 切点
     */
    @After("controllerAspect()")
    public void doAfter(JoinPoint joinPoint) {
        UserInfoDTO userInfo  = currentUserInfo.get();
        //登入login操作 前置通知时用户未校验 所以session中不存在用户信息
        if (userInfo == null) {
            String loginId = GuserCookieUtil.getLoginId(request);
            userInfo = dubboService.userInfoService.getUserInfoByLoginId(loginId).getDataResult();
            if (userInfo == null) {
                return;
            }
        }
        Object[] args = joinPoint.getArgs();
        System.out.println(args);

        String desc = "";
        String type = "info";                       //日志类型(info:入库,error:错误)
        String remoteAddr = request.getRemoteAddr();//请求的IP
        String requestUri = request.getRequestURI();//请求的Uri
        String method = request.getMethod();        //请求的方法类型(post/get)
        Map<String, String[]> paramsMap = request.getParameterMap(); //请求提交的参数
        try {
            desc = getControllerMethodDescription(request,joinPoint);
        } catch (Exception e) {
            e.printStackTrace();
        }
        // debug模式下打印JVM信息。
//        long beginTime = beginTimeThreadLocal.get().getTime();//得到线程绑定的局部变量(开始时间)
//        long endTime = System.currentTimeMillis();    //2、结束时间
//        if (logger.isDebugEnabled()) {
//            logger.debug("计时结束:{}  URI: {}  耗时: {}   最大内存: {}m  已分配内存: {}m  已分配内存中的剩余空间: {}m  最大可用内存: {}m",
//                    new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(endTime),
//                    request.getRequestURI(),
//                    DateUtils.formatDateTime(endTime - beginTime),
//                    Runtime.getRuntime().maxMemory() / 1024 / 1024,
//                    Runtime.getRuntime().totalMemory() / 1024 / 1024,
//                    Runtime.getRuntime().freeMemory() / 1024 / 1024,
//                    (Runtime.getRuntime().maxMemory() - Runtime.getRuntime().totalMemory() + Runtime.getRuntime().freeMemory()) / 1024 / 1024);
//        }

        OperateLogDO log = new OperateLogDO();
        log.setDesc(desc);
        log.setType(type);
        log.setRemoteAddr(remoteAddr);
        log.setRequestUri(requestUri);
        log.setMethod(method);
        log.setMapToParams(paramsMap);
        log.setUserName(userInfo.getName());
        log.setUserId(userInfo.getLoginId());
//        Date operateDate = beginTimeThreadLocal.get();
//        log.setOperateDate(operateDate);
//        log.setTimeout(DateUtils.formatDateTime(endTime - beginTime));

        //1.直接执行保存操作
        //this.logService.createSystemLog(log);

        //2.优化:异步保存日志
        //new SaveLogThread(log, logService).start();

        //3.再优化:通过线程池来执行日志保存
        threadPoolTaskExecutor.execute(new SaveLogThread(log,logManager));
        logThreadLocal.set(log);
    }

    /**
     * 异常通知
     *
     * @param joinPoint
     * @param e
     */
    @AfterThrowing(pointcut = "controllerAspect()", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Throwable e) {
        OperateLogDO log = logThreadLocal.get();
        if (log != null) {
            log.setType("error");
            log.setException(e.toString());
            new UpdateLogThread(log,logManager).start();
        }
    }

    /**
     * 获取注解中对方法的描述信息 用于Controller层注解
     *
     * @param joinPoint 切点
     * @return 方法描述
     */
    public static String getControllerMethodDescription(HttpServletRequest request,JoinPoint joinPoint) throws IllegalAccessException, InstantiationException {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        SystemControllerLog controllerLog = method
                .getAnnotation(SystemControllerLog.class);
        String desc = controllerLog.description();
        List<String> list = descFormat(desc);
        for (String s : list) {
            //根据request的参数名获取到参数值,并对注解中的{}参数进行替换
            String value=request.getParameter(s);
            desc = desc.replace("{"+s+"}", value);
        }
        return desc;
    }

    /**
     * 获取日志信息中的动态参数
     * @param desc
     * @return
     */
    private static List<String> descFormat(String desc){
        List<String> list = new ArrayList<String>();
        Pattern pattern = Pattern.compile("\\{([^\\}]+)\\}");
        Matcher matcher = pattern.matcher(desc);
        while(matcher.find()){
            String t = matcher.group(1);
            list.add(t);
        }
        return list;
    }
    /**
     * 保存日志线程
     *
     * @author lin.r.x
     */
    private static class SaveLogThread implements Runnable {
        private OperateLogDO log;
        private LogManager logManager;

        public SaveLogThread(OperateLogDO log, LogManager logManager) {
            this.log = log;
            this.logManager = logManager;
        }

        @Override
        public void run() {
            logManager.insert(log);
        }
    }

    /**
     * 日志更新线程
     *
     * @author lin.r.x
     */
    private static class UpdateLogThread extends Thread {
        private OperateLogDO log;
        private LogManager logManager;

        public UpdateLogThread(OperateLogDO log, LogManager logManager) {
            super(UpdateLogThread.class.getSimpleName());
            this.log = log;
            this.logManager = logManager;
        }

        @Override
        public void run() {
            this.logManager.update(log);
        }
    }
}

4.实现AsyncConfigurer接口并重写AsyncConfigurer方法,并返回一个ThreadPoolTaskExecutor,这样我们就得到了一个基于线程池的TaskExecutor.

在Executor配置类中增加@EnableAsync注解,开启异步支持。

package com.guahao.wcp.gops.home.configuration;

import com.alibaba.dubbo.common.logger.Logger;
import com.alibaba.dubbo.common.logger.LoggerFactory;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.lang.reflect.Method;
import java.util.concurrent.Executor;

/**
 * @program: wcp
 * @description: 配置类实现AsyncConfigurer接口并重写AsyncConfigurer方法,并返回一个ThreadPoolTaskExecutor
 * @author: Cay.jiang
 * @create: 2018-03-12 17:27
 **/

//声明这是一个配置类
@Configuration
//开启注解:开启异步支持
@EnableAsync
public class TaskExecutorConfigurer implements AsyncConfigurer {
    private static final Logger log = LoggerFactory.getLogger(TaskExecutorConfigurer.class);
    @Bean
    //配置类实现AsyncConfigurer接口并重写AsyncConfigurer方法,并返回一个ThreadPoolTaskExecutor
    //这样我们就得到了一个基于线程池的TaskExecutor
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        //如果池中的实际线程数小于corePoolSize,无论是否其中有空闲的线程,都会给新的任务产生新的线程
        taskExecutor.setCorePoolSize(5);
        //连接池中保留的最大连接数。Default: 15 maxPoolSize
        taskExecutor.setMaxPoolSize(10);
        //线程池所使用的缓冲队列
        taskExecutor.setQueueCapacity(25);
        //等待所有线程执行完
        taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
        taskExecutor.initialize();
        return taskExecutor;
    }
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new WcpAsyncExceptionHandler();
    }
    /**
     * 自定义异常处理类
     * @author hry
     *
     */
    class WcpAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
        //手动处理捕获的异常
        @Override
        public void handleUncaughtException(Throwable throwable, Method method, Object... obj) {
            System.out.println("-------------》》》捕获到线程异常信息");
            log.info("Exception message - " + throwable.getMessage());
            log.info("Method name - " + method.getName());
            for (Object param : obj) {
                log.info("Parameter value - " + param);
            }
        }

    }
}

5.logManager调用日志DAO操作,具体的mybatis实现就不写了。

package com.guahao.wcp.core.manager.operatelog.impl;

import com.guahao.wcp.core.dal.dataobject.OperateLogDO;
import com.guahao.wcp.core.dal.mapper.OperateLogMapper;
import com.guahao.wcp.core.manager.operatelog.LogManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service("logManager")
public class LogManagerImpl implements LogManager {
    

    @Autowired
    private OperateLogMapper operateLogDAO;
    
    @Override
    public int insert(OperateLogDO log) {

        System.out.println("新增操作日志:"+log);
        return operateLogDAO.insert(log);
    }
    
    @Override
    public int update(OperateLogDO log) {
        //暂不实现
        //return this.logDao.updateByPrimaryKeySelective(log);
        System.out.println("更新操作日志:"+log);
        return 1;
    }

}

6.使用范例ApplicationController方法中添加日志注解

@RequestMapping(value = "/add.json", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"})
    @ResponseBody
    @SystemControllerLog (description = "【应用管理】新增应用{applicationName}")
    public BaseJson add(@ModelAttribute("application") ApplicationDO applicationDO, @ModelAttribute("team") TeamDO teamDO) {

.......
}

7.日志数据入库结果

8.日志结果展示,grafana配置,vue编写日志查询界面

更多推荐

【生物信息学】奇异值分解(SVD)

目录一、奇异值分解(SVD)二、Python实现1.调包np.linalg.svd()2.自定义三、SVD实现链路预测一、奇异值分解(SVD)SVD分解核心思想是通过降低矩阵的秩来提取出最重要的信息,实现数据的降维和去噪。ChatGPT:SVD(奇异值分解)是一种常用的矩阵分解方法,它可以将一个矩阵分解为三个矩阵的乘积

Scanner类用法(学习笔记)

Scanner类用法(学习笔记,后续会补充)1.next()用法packagecom.yushifu.scanner;importjava.util.Scanner;//utiljava工具包//Scanner类(获取用户的输入)Scanners=newScanner(System.in);//通过Scanner类的n

论文阅读 - Outlier detection in social networks leveraging community structure

目录摘要1.Introduction2.Relatedworks3.Preliminaries3.1.模块化度量3.2.Classesofoutliers3.2.1.点异常3.2.2.Contextualanomalies3.2.3.Collectiveanomalies3.3.Problemdefinition3.4

PBR纹理的10种贴图

PBR是基于物理的渲染的首字母缩写。它试图通过模拟材料如何吸收和反射光,以模仿现实世界中的光流的方式产生视觉效果。最近的游戏引擎由于其逼真的效果而越来越多地使用PBR纹理。对于实时渲染,它们被认为是真实世界场景的最佳近似值。推荐:用NSDT编辑器快速搭建可编程3D场景为了创建效果,大多数渲染引擎都有其独特的工作流程。但

NExT-GPT: Any-to-Any Multimodal LLM论文笔记

论文https://arxiv.org/pdf/2309.05519.pdf代码https://github.com/NExT-GPT/NExT-GPT/tree/main1.Motivation现有的多模态大模型大都只是支持输入端的多模态(Text、Image、Video、Audio等),但是输出端都是Text。也有

web大作业 比赛报名页面+ 团队介绍页面 制作

web大作业比赛报名页面+团队介绍页面制作【附源代码】文章目录web大作业比赛报名页面+团队介绍页面制作【附源代码】前言报名界面效果图如下:代码实现计时器效果实现(jquery+boostrap)团队介绍页面模拟框代码:CSS代码前言之前没看过看过上一篇文章的小伙伴,可以看一下之前的文章,里面有一些组件设计是下面没有提

Navidrome - 开源音乐服务器【打造属于自己的音乐播放器】「端口映射」随时随地想听就听

转载自cpolar极点云文章:Navidrome-开源音乐服务器【打造属于自己的音乐播放器】「端口映射」随时随地想听就听1.前言不知从何时开始,我们能用的音乐软件越来越少,笔者使用小米手机很久了,自从小米手机的自带音乐播放器变成了QQ音乐,笔者手机里很多的音乐就无法再自由畅听,要求付费加会员听歌,不然就得忍受被剪切的试

网络编程【TCP单向通信、TCP双向通信、一对多应用、一对多聊天服务器】(二)-全面详解(学习总结---从入门到深化)

目录Java网络编程中的常用类TCP通信的实现和项目案例TCP通信入门案例TCP单向通信TCP双向通信创建点对点的聊天应用一对多应用一对多聊天服务器Java网络编程中的常用类Java为了跨平台,在网络应用通信时是不允许直接调用操作系统接口的,而是由java.net包来提供网络功能。下面我们来介绍几个java.net包中

【JAVA】多态的概念与实际利用

个人主页:【😊个人主页】系列专栏:【❤️初识JAVA】前言在面向对象(OOP)的程序设计语言中,多态与封装、继承合称为OOP的三大特性。在今天,我们就来学习一下JAVA中的多态是什么样子的。、多态指一个对象在不同情况下可以表现出不同的行为。Java多态性分为两种:编译时多态性(静态多态性)和运行时多态性(动态多态性)

【三维重建】3D Gaussian Splatting:实时的神经场渲染

文章目录摘要一、前言二、相关工作1.传统的场景重建与渲染2.神经渲染和辐射场3.基于点的渲染和辐射场4.*什么是Tile-basedrasterizer(快速光栅化)三、OVERVIEW四、可微的三维高斯Splatting五、三维高斯自适应密度控制的优化1.优化2.高斯的自适应控制六、高斯分布的快速可微光栅化器(拓展)

DevSecOps内置安全保护

前言随着DevOps的发展,DevOps大幅提升了企业应用迭代的速度。但同时,安全如果不能跟上步伐,不仅会抵消DevOps变革带来的提升,拖慢企业数字化转型进程,还会导致漏洞与风险不约而至。所以安全能力在全球范围内受到的重视越来越高,软件开发内生的安全性成为评价企业DevOps成熟度水平的重要指标。一直以来,业界长期重

热文推荐