• 审计日志实现


    审计日志实现

    目标

    记录用户行为:

    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就省掉了(因为是封装好的功能模块)

  • 相关阅读:
    dumpsys
    阿里云云效流水线体验
    停车入场城市排行榜1
    第三方企业号对接工作
    PHP搭建(windows64+apache2.4.7+mysql-5.6+php5.5)
    十大编程算法助程序员走上高手之路
    数据库的最简单实现
    JavaScript 开发的45个经典技巧
    常用meta整理
    Shell脚本编程初体验
  • 原文地址:https://www.cnblogs.com/chenglc/p/14860058.html
Copyright © 2020-2023  润新知