审计日志实现

审计日志实现

目标

记录用户行为:

  1. 用户A 在xx时间 做了什么
  2. 用户B 在xx时间 改变了什么

针对以上场景,需要记录以下一些接口信息:

  1. 时间
  2. ip
  3. 用户
  4. 入参
  5. 响应
  6. 改变数据内容描述
  7. 标签-区分领域

效果

  1. 将此类信息单独输出log(可不选)
  2. 持久化储存,便于查询追踪

设计

  1. 提供两个信息记录入口:注解和api调用
  2. 信息通过log记录,输出到log和mq
  3. 消费mq数据,解析到ES做持久化
  4. 查询:根据时间,操作名称,标签进行检索

示意图
image

实现

属性封装

LcpAuditLog:数据实体

@Builder
@Data
public class LcpAuditLog implements Serializable {
    private static final long serialVersionUID = -6309732882044872298L;

    /**
     * 操作人
     */
    private String operator;
    /**
     * 操作(可指定,默认方法全路径)
     */
    private String operation;
    /**
     * 操作时间
     */
    private Date operateTime;
    /**
     * 参数(可选)
     */
    private String params;
    /**
     * ip(可选)
     */
    private String ip;

    /**
     * 返回(可选)
     */
    private String response;

    /**
     * 标签
     */
    private String tag;

    /**
     * 影响数据
     */
    private String influenceData;

}
定义注解AuditLog

AuditLog注解,用于标记哪些方法需要做审计日志,与业务解耦。仅记录基本信息:时间,用户,操作,入参,响应。

@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuditLog {
    /**
     * 操作标识
     */
    @AliasFor(value = "value")
    String operation() default "";

    @AliasFor(value = "operation")
    String value() default "";

    /**
     * 标签
     * @return
     */
    String tag() default "";
}

注解实现类

常规做法,借助aop实现。

@Slf4j
@Aspect
@Order(2)
@Configuration
public class AuditLogAspect {

    /**
     * 单个参数最大长度
     */
    private static final int PARAM_MAX_LENGTH = 5000;
    private static final int RESULT_MAX_LENGTH = 20000;

    @Resource(name = "logService")
    private LogService logService;

    public AuditLogAspect() {
        log.info("AuditLogAspect is init");
    }

    /**
     * 后置通知,当方法正常运行后触发
     *
     * @param joinPoint
     * @param auditLog  审计日志
     * @param result
     */
    @AfterReturning(pointcut = "@annotation(auditLog)", returning = "result")
    public void doAfterReturning(JoinPoint joinPoint, AuditLog auditLog, Object result) {
        doPrintLog(joinPoint, auditLog, result);
    }

    /**
     * 方法抛出异常后通知
     *
     * @param joinPoint
     * @param auditLog
     * @param throwable
     */
    @AfterThrowing(value = "@annotation(auditLog)", throwing = "throwable")
    public void AfterThrowing(JoinPoint joinPoint, AuditLog auditLog, Throwable throwable) {
        doPrintLog(joinPoint, auditLog, throwable.getMessage());
    }

    /**
     * 打印安全日志
     *
     * @param joinPoint
     * @param auditLog
     * @param result
     */
    private void doPrintLog(JoinPoint joinPoint, AuditLog auditLog, Object result) {
        try {
            String approveUser = getUser();
            String ip = getHttpIp();
            Object[] args = joinPoint.getArgs();
            String methodName = joinPoint.getTarget().getClass().getName()
                    + "."
                    + joinPoint.getSignature().getName();

            String tag = auditLog.tag() == null ? "" : auditLog.tag();
            String operation = auditLog.operation();
            operation = StringUtils.isEmpty(operation) ? auditLog.value() : operation;
            operation = StringUtils.isEmpty(operation) ? methodName : operation;
            String resultString = JsonUtils.toJSONString(result);
            if (resultString != null && resultString.length() > RESULT_MAX_LENGTH) {
                resultString = resultString.substring(0, RESULT_MAX_LENGTH);
            }
            logService.writeAuditLog(LcpAuditLog
                    .builder()
                    .ip(ip)
                    .operateTime(new Date())
                    .operator(approveUser)
                    .operation(operation)
                    .params(generateParamDigest(args))
                    .response(resultString)
                    .tag(tag)
                    .build());
        } catch (Throwable t) {
            log.error("AuditLogAspect 打印审计日志失败,失败原因:", t);
        }
    }

    private String getHttpIp() {
        try {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            return request.getRemoteAddr();
        } catch (Exception e) {
            //jsf调用时没有http上游ip,记录jsf ip 没有意义
            return "";
        }
    }


    /**
     * 获取用户PIN
     *
     * @return
     */
    private String getUser() {
        // ...
    }

    /**
     * 生成参数摘要字符串
     *
     * @since 1.1.12
     */
    private String generateParamDigest(Object[] args) {
        StringBuffer argSb = new StringBuffer();
        for (Object arg : args) {
            if (!(arg instanceof HttpServletRequest)) {
                if (argSb.length() > 0) {
                    argSb.append(",");
                }
                String argString = JsonUtils.toJSONString(arg);
                //避免超大参数
                if (argString != null && argString.length() > PARAM_MAX_LENGTH) {
                    argString = argString.substring(0, PARAM_MAX_LENGTH);
                }
                argSb.append(argString);
            }
        }
        return argSb.toString();
    }

}

定义操作API

有了注解还需要API?

  1. 注解可以解决大部分情况,但是个别场景需要定制化记录
  2. 注解的解析结果也需要业务实现,代码层面业务解耦

service

public interface LogService {
    /**
     * 输出审计日志
     *
     * <pre>
     *     ex:
     *     writeAuditLog(LcpAuditLog
     *                 .builder()
     *                 .operation(operation)
     *                 .operator(operator)
     *                 .operateTime(new Date())
     *                 .ip(getLocalHost())
     *                 .influenceData(influenceData)
     *                 .build());
     * </pre>
     *
     * @param log
     */
    void writeAuditLog(LcpAuditLog log);

    /**
     * 记录操作日志
     * <pre>
     *     ex1:recordOperationLog("刁德三","删除用户","{userId:12,userName:lao sh an}");
     *     ex2:recordOperationLog("di da","deleteUser","{userId:12,userName:lao sh an}");
     * </pre>
     *
     * @param operator      操作人
     * @param operation     动作
     * @param influenceData 影响数据
     */
    void recordOperationLog(String operator, String operation, String influenceData);

    /**
     * 记录操作日志
     * <pre>
     *     ex:recordOperationLog("刁德三","删除用户","{userId:12,userName:lao sh an}","运维操作");
     * </pre>
     *
     * @param operator      操作人
     * @param operation     动作
     * @param influenceData 影响数据
     * @param tag           标签
     */
    void recordOperationLog(String operator, String operation, String influenceData, String tag);
}
业务实现ServiceImpl
@Slf4j
@Service("logService")
public class LogServiceImpl implements LogService {
    @Override
    public void writeAuditLog(LcpAuditLog lcpAuditLog) {
        try {
            if (log.isInfoEnabled()) {
                log.info(JsonUtils.toJSONString(lcpAuditLog));
            }
        } catch (Throwable e) {
            //借助外部输出异常log,因为当前类的log被特殊 监控!!
            PrintLogUtil.printErrorLog("LcpLogServiceImpl 打印审计日志失败,e=", e);
        }
    }

    @Override
    public void recordOperationLog(String operator, String operation, String influenceData) {
        this.writeAuditLog(LcpAuditLog
                .builder()
                .operation(operation)
                .operator(operator)
                .operateTime(new Date())
                .ip(getLocalHost())
                .influenceData(influenceData)
                .build());
    }

    @Override
    public void recordOperationLog(String operator, String operation, String influenceData, String tag) {
        this.writeAuditLog(LcpAuditLog
                .builder()
                .operation(operation)
                .operator(operator)
                .operateTime(new Date())
                .ip(getLocalHost())
                .influenceData(influenceData)
                .tag(tag)
                .build());
    }

    private static String getLocalHost() {
        try {
            return InetAddress.getLocalHost().getHostAddress();
        } catch (Exception e) {
            PrintLogUtil.printErrorLog("LcpLogServiceImpl 打印审计日志失败,e=", e);
            return "";
        }
    }
}

业务代码只是一句log.info()???

kafka呢?

sl4j配置及kafka写入

sl4f2.0有封装对kafka的写入能力,具体实现:

引入必要的pom

        <dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka-clients</artifactId>
            <version>1.0.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>

log配置

sl4j2.xml

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <properties>
        <property name="LOG_HOME">/data/Logs/common</property>
        <property name="FILE_NAME">audit</property>
    </properties>

    <Appenders>
        <RollingFile name="asyncRollingFile" fileName="${LOG_HOME}/${FILE_NAME}.log"
                     filePattern="${LOG_HOME}/$${date:yyyy-MM}/${FILE_NAME}-%d{yyyy-MM-dd}-%i.log.gz">
            <PatternLayout pattern="%-d{yyyy-MM-dd HH:mm:ss}[ %t:%r ] [%X{traceId}] - [%-5p] %c-%M:%L - %m%n%throwable{full}"/>
            <Policies>
                <TimeBasedTriggeringPolicy/>
                <SizeBasedTriggeringPolicy size="100 MB"/>
            </Policies>
            <DefaultRolloverStrategy max="20"/>
        </RollingFile>
        <Kafka name="auditLog" topic="log_jmq" syncSend="false">
            <PatternLayout pattern="%m%n"/>
            <Property name="client.id">client.id</Property>
            <Property name="retries">3</Property>
            <Property name="linger.ms">1000</Property>
            <Property name="bootstrap.servers">nameserver:port</Property>
            <Property name="compression.type">gzip</Property>
        </Kafka>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] [%X{requestId}] %-5level %l - %msg%n"/>
        </Console>
    </Appenders>

    <Loggers>
        <Logger name="org.apache.kafka" level="info" />
        <!--操作日志-->
        <AsyncLogger name="com.service.impl.LogServiceImpl" level="INFO" additivity="false">
            <AppenderRef ref="auditLog"/>
            <AppenderRef ref="asyncRollingFile"/>
            <AppenderRef ref="Console"/>
        </AsyncLogger>
    </Loggers>
</Configuration>

至此,可以完成对审计日志的log输出和mq写入,后续的mq消费,写入es就省掉了(因为是封装好的功能模块)

原文地址:https://www.cnblogs.com/chenglc/p/14860058.html