审计日志

使用spring AOP实现的审计日志

环境准备

jdk1.8,springboot

依赖如下:

<!-- 添加aspectj -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
    <version>2.1.4.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.9.2</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.2</version>
</dependency>

项目结构

我写这个项目时是后期加入,导致多个服务需要用,所以将相关的都存入一个包中

PMD java审计工具 java审计日志_IP

自定义审计注解

package com.bocloud.devops.accesslog;

import com.paas.bocloud.accesslog.InterfaceType;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = { ElementType.METHOD })
public @interface Audit {
    //需要得到什么信息,就定义什么,看自己需求
	/**
     * 模块类
     */
    Module module();
	
	/**
     * 模块名称
     */
    String object();
	
    /**
     * 操作
     */
    String operation();
	
     /**
     * 操作类型
     */
    InterfaceType type();

    //BoCloudFunction function() default BoCloudFunction.NULL;

    String detail() default "";
	
     /**
     * 扩展信息
     */
    Class<?>[] entity() default {};
}

定义切面类

package com.bocloud.devops.accesslog;

import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.paas.bocloud.common.http.response.DataResponse;
import com.paas.bocloud.common.jwt.util.BaseContextHandler;
import net.sf.json.JSONArray;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
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.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Objects;

@Aspect
@Component
public class AuditAop {

    private static final Logger LOGGER = LoggerFactory.getLogger(AuditAop.class);

    @Autowired
    private RabbitmqClient rabbitmqClient;

    @Autowired
    private PlatFormFeignClient platFormFeignClient;


    // 操作发起者
    ThreadLocal<Integer> userId = new ThreadLocal<>();
    // 操作租户ID
    ThreadLocal<Integer> tenantId = new ThreadLocal<>();
    // 功能模块
    ThreadLocal<String> moudleCode = new ThreadLocal<>();
    // 操作类型
    ThreadLocal<Integer> operateType = new ThreadLocal<>();
    // IP地址
    ThreadLocal<String> ip = new ThreadLocal<>();
    // 操作时间点
    ThreadLocal<String> operateTime = new ThreadLocal<>();
    // 操作信息实体
    ThreadLocal<AccessLog> AccessLog = new ThreadLocal<>();
    // 对CIS,额外的菜单id信息extension
    ThreadLocal<String> extension = new ThreadLocal<>();
    ThreadLocal<Long> startTime = new ThreadLocal();

    // 声明AOP切入点
    @Pointcut("@annotation(com.bocloud.devops.accesslog.Audit)")
    public void audit() {
        System.out.println(1);
    }

    @Before("audit()")
    public void beforeExec(JoinPoint joinPoint) {
    }

    @After("audit()")
    public void afterExec(JoinPoint joinPoint) {
    }

    @Around("audit()")
    public Object aroundExec(ProceedingJoinPoint pjp) throws Throwable {
        //开始时间
        startTime.set(System.currentTimeMillis());
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        extension.set(request.getParameter("extension"));
        operateTime.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
        //获得requestIp(请求IP)
        ip.set(IpUtil.getIpAddress(request));

        MethodSignature ms = (MethodSignature) pjp.getSignature();
        Method method = ms.getMethod();
        // 获取注解的参数信息
        Audit auditAnno = method.getAnnotation(Audit.class);
        moudleCode.set(auditAnno.module().getServiceName());
        operateType.set(auditAnno.type().getIndex());
        //为实体类赋值,以便存入数据库
        AccessLog accessLog=new AccessLog(); 
        //获取操作用户 (需判断userId是否为空,是否登录过期)
        if (BaseContextHandler.getUserId()!=null){
            accessLog.setUserId(Integer.valueOf(BaseContextHandler.getUserId()));
            accessLog.setUserName(BaseContextHandler.getUserName());
        }
        accessLog.setModule(moudleCode.get());//获得模块名
        accessLog.setAction(auditAnno.operation());//获得操作
        //获得参数
        Object[] args = pjp.getArgs();
        accessLog.setDetail(JSON.toJSONString(args));
        accessLog.setGmtCreate(new Date());//获得操作日期
        accessLog.setObject(auditAnno.object());
        accessLog.setRequestUrl(request.getRequestURI());//访问地址
        accessLog.setInterfaceType(operateType.get());
        accessLog.setRequestIp(ip.get());
        accessLog.setResponseIp(IpUtil.getLocalIp());//responseIp(响应IP)
        //获得此接口的响应结果 需存库写入,不需要最好不要写,在最后return 
        //pjp.proceed()获得真正被代理对象的业务方法的返回值
        Object rtn = pjp.proceed();
        //获得耗时
        accessLog.setCost(System.currentTimeMillis() - startTime.get());
        startTime.remove();
        //获得返回信息
        accessLog.setResult(JSON.toJSONString(rtn));
        AccessLog.set(accessLog);
       	return rtn;
    }
    /**
     * 带参返回
     */
    @AfterReturning(pointcut = "audit()")
    public void doAfterReturning() {
        try {
            // 返回码SUCCESS才去审计记录
            if (ResponseContent.RESPCODE_SUCCESS.equals(rc.getRespCode())) {
                if (null == msg.get() && extension.get() != null) {
                    LOGGER.warn("Warning occured, because msg is null!");
                } else if (!(operateType.get() == Operate.FETCH_DATA
                        && (extension.get() == null || extension.get().isEmpty()))) {
                    //添加到数据库
                    auditService.insert(AccessLog.get());
                }
            }
        } catch (Exception e) {
            LOGGER.warn("Warning occured while afterReturning, cause by: ", e);
        }

    }

    /**
     * 不带参返回
     */
    @AfterReturning(pointcut = "audit()")
    public void doAfterReturning(JoinPoint joinPoint) {
        LOGGER.debug("afterReturning without returnType..");
    }

    @AfterThrowing(pointcut = "audit()", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Throwable e) {
        LOGGER.error("Error occured, cause by {}", e.getMessage());
    }

获得请求IP,响应Ip工具类 (可以按需求索取)

package com.bocloud.devops.accesslog;

import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Enumeration;

public class IpUtil {
    
    /**
     * 获取本地IP地址
     *
     */
    public static String getLocalIp() {
        if (isWindowsOS()) {
            String hostAddress = null;
            try {
                hostAddress = InetAddress.getLocalHost().getHostAddress();
            }catch (UnknownHostException e){
                // 获取windows ip 地址发送异常
            }
            return hostAddress;
        } else {
            return getLinuxLocalIp();
        }
    }

    /**
     * 判断操作系统是否是Windows
     *
     * @return
     */
    public static boolean isWindowsOS() {
        String windowsOsName = "windows";
        boolean isWindowsOs = false;
        String osName = System.getProperty("os.name");
        if (osName.toLowerCase().contains(windowsOsName)) {
            isWindowsOs = true;
        }
        return isWindowsOs;
    }

    /**
     * 获取本地Host名称
     */
    public static String getLocalHostName() throws UnknownHostException {
        return InetAddress.getLocalHost().getHostName();
    }

    /**
     * 获取Linux下的IP地址
     *
     * @return IP地址
     * @throws SocketException
     */
    private static String getLinuxLocalIp(){
        String ip = "";
        try {
            for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements();) {
                NetworkInterface intf = en.nextElement();
                String name = intf.getName();
                if (!name.contains("docker") && !name.contains("lo")) {
                    for (Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements();) {
                        InetAddress inetAddress = enumIpAddr.nextElement();
                        if (!inetAddress.isLoopbackAddress()) {
                            String ipaddress = inetAddress.getHostAddress().toString();
                            if (!ipaddress.contains("::") && !ipaddress.contains("0:0:") && !ipaddress.contains("fe80")) {
                                ip = ipaddress;
                            }
                        }
                    }
                }
            }
        } catch (SocketException ex) {
            ip = "127.0.0.1";
        }
        return ip;
    }

    /**
     * 获取用户真实IP地址,不使用request.getRemoteAddr();的原因是有可能用户使用了代理软件方式避免真实IP地址,
     *
     * 可是,如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP值,究竟哪个才是真正的用户端的真实IP呢?
     * 答案是取X-Forwarded-For中第一个非unknown的有效IP字符串。
     *
     * 如:X-Forwarded-For:192.168.1.110, 192.168.1.120, 192.168.1.130,
     * 192.168.1.100
     *
     * 用户真实IP为: 192.168.1.110
     *
     * @param request
     * @return
     */
    public static String getIpAddress(HttpServletRequest request) {
        String unknownIpName = "unknown";
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || unknownIpName.equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || unknownIpName.equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || unknownIpName.equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || unknownIpName.equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || unknownIpName.equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }

}

相关枚举类

只是举例,按实际需求定义

package com.bocloud.devops.accesslog;

import lombok.Getter;

/**
 * 各个模块的consul请求路径和api路径
 */
@Getter
public enum Module {
  Workflow("devopsworkflowapi", "devops-workflow"),
  WorkflowEngine("devopsworkflowengineapi", "devops-workflow-engine"),
  Platform("devopsplatformapi", "devops-platform"),
  Jira("devopsjiraapi", "devops-jira"),
  Boclink("devopsboclinkapi", "devops-message"),
  Pipeline("devopspipelineapi","devops-pipeline"),

  Upms("upmsapi", "paas-basic-upms"),
  Runtime("runtimeapi", "paas-runtime"),
  Application("applicationapi", "paas-application");


  private Module(String gateWay, String serviceName) {
    this.gateWay = gateWay;
    this.serviceName = serviceName;
  }

  private String gateWay;
  private String serviceName;
}




package com.paas.bocloud.accesslog;

import lombok.Getter;

@Getter
public enum InterfaceType {
    INSERT("inster", 1, "新增接口"),
    DELETE("delete", 2, "删除接口"),
    UPDATE("update", 3, "修改接口"),
    SELECT("select", 4, "查询接口");

    private String interfaceType;
    private int index;
    private String name;

    private InterfaceType(String interfaceType, int index, String name) {
        this.interfaceType = interfaceType;
        this.index = index;
        this.name = name;
    }

}

定义审计信息实体类

package com.bocloud.devops.accesslog;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

@Data
public class AccessLog implements Serializable {

    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    /**
     * 操作  中文描述
     */
    private String action;

    /**
     * 耗时
     */
    private Long cost;

    /**
     * 请求参数
     */
    private String detail;

    /**
     * 操作日期
     */
    private Date gmtCreate;

    /**
     * 服务 例如:服务名
     */
    private String module;

    private String object; // 接口模块

    /**
     * 请求URL
     */
    private String requestUrl;

    /**
     * 请求IP
     */
    private String requestIp;

    /**
     * 响应IP
     */
    private String responseIp;

    private String result;

    /**
     * 接口类型 1新增类接口 2删除类接口 3修改类接口 4查询类接口
     */
    private Integer interfaceType;

    /**
     * 用户ID
     */
    private Integer userId;

    /**
     * 用户名
     */
    private String userName;

}

举例:

public class controller{
	@RequestMapping(value = "/ceshi", method = { RequestMethod.GET})
    @Audit(module = Module.Pipeline,object ="审计日志" ,operation = "测试审计日志",type = InterfaceType.SLECT)
    public DataResponse<Object> ceshi(){

        return DataResponse.success("审计日志测试");
    }
}