操作日志注解模块

功能说明

@CommonLog 是XX框架的自定义日志注解,通过Spring AOP技术自动记录方法操作日志,实现无侵入式的操作审计。


注解定义

java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CommonLog {
    /**
     * 日志名称,如"新增用户"
     */
    String value() default "未命名";
}

注解属性

  • value: 操作名称,用于描述日志记录的业务动作

AOP拦截机制

拦截流程

复制代码
客户端请求 → Controller方法(@CommonLog标记) → AOP切面拦截 → 记录日志 → 执行原方法

AOP切面配置

java 复制代码
@Aspect
@Order
@Component
public class DevLogAop {

    /**
     * 日志切入点
     */
    @Pointcut("@annotation(vip.xx.common.annotation.CommonLog)")
    private void getLogPointCut() {
    }

    /**
     * 操作成功返回结果记录日志
     */
    @AfterReturning(pointcut = "getLogPointCut()", returning = "result")
    public void doAfterReturning(JoinPoint joinPoint, Object result) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        CommonLog commonLog = method.getAnnotation(CommonLog.class);
        String userName = "未知";
        try {
            SaBaseLoginUser loginUser = StpLoginUserUtil.getLoginUser();
            if(ObjectUtil.isNotNull(loginUser)) {
                userName = loginUser.getName();
            }
        } catch (Exception ignored) {
        }
        // 异步记录日志
        DevLogUtil.executeOperationLog(commonLog, userName, joinPoint, JSONUtil.toJsonStr(result));
    }

    /**
     * 操作发生异常记录日志
     */
    @AfterThrowing(pointcut = "getLogPointCut()", throwing = "exception")
    public void doAfterThrowing(JoinPoint joinPoint, Exception exception) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        CommonLog commonLog = method.getAnnotation(CommonLog.class);
        String userName = "未知";
        try {
            SaBaseLoginUser loginUser = StpLoginUserUtil.getLoginUser();
            if(ObjectUtil.isNotNull(loginUser)) {
                userName = loginUser.getName();
            }
        } catch (Exception ignored) {
        }
        //异步记录日志
        DevLogUtil.executeExceptionLog(commonLog, userName, joinPoint, exception);
    }
}

使用方法

在需要记录日志的Controller方法上添加@CommonLog注解:

java 复制代码
@RestController
@RequestMapping("/user")
public class UserController {
    
    @PostMapping
    @CommonLog("新增用户")
    public CommonResult<?> addUser(@RequestBody UserAddParam param) {
        userService.add(param);
        return CommonResult.ok();
    }
}

核心技术点

1. 异步日志记录

问题背景:日志写入数据库是IO操作,同步执行会阻塞主线程,影响接口响应性能。

解决方案 :项目中使用Hutool的ThreadUtil.execute()实现异步执行。

项目中的实现方式DevLogUtil.java):

java 复制代码
public class DevLogUtil {

    private static final DevLogService devLogService = SpringUtil.getBean(DevLogService.class);

    /**
     * 记录操作日志(异步)
     */
    public static void executeOperationLog(CommonLog commonLog, String userName, JoinPoint joinPoint, String resultJson) {
        HttpServletRequest request = CommonServletUtil.getRequest();
        DevLog devLog = genBasOpLog();
        ThreadUtil.execute(() -> {
            devLog.setCategory(DevLogCategoryEnum.OPERATE.getValue());
            devLog.setName(commonLog.value());
            devLog.setExeStatus(DevLogExeStatusEnum.SUCCESS.getValue());
            devLog.setClassName(joinPoint.getTarget().getClass().getName());
            devLog.setMethodName(joinPoint.getSignature().getName());
            devLog.setReqMethod(request.getMethod());
            devLog.setReqUrl(request.getRequestURI());
            devLog.setParamJson(CommonJoinPointUtil.getArgsJsonString(joinPoint));
            devLog.setResultJson(resultJson);
            devLog.setOpTime(DateTime.now());
            devLog.setOpUser(userName);
            creatLogSignValue(devLog);
            devLogService.save(devLog);
        });
    }

    /**
     * 记录异常日志(异步)
     */
    public static void executeExceptionLog(CommonLog commonLog, String userName, JoinPoint joinPoint, Exception exception) {
        HttpServletRequest request = CommonServletUtil.getRequest();
        DevLog devLog = genBasOpLog();
        ThreadUtil.execute(() -> {
            devLog.setCategory(DevLogCategoryEnum.EXCEPTION.getValue());
            devLog.setName(commonLog.value());
            devLog.setExeStatus(DevLogExeStatusEnum.FAIL.getValue());
            devLog.setExeMessage(ExceptionUtil.stacktraceToString(exception, Integer.MAX_VALUE));
            devLog.setClassName(joinPoint.getTarget().getClass().getName());
            devLog.setMethodName(joinPoint.getSignature().getName());
            devLog.setReqMethod(request.getMethod());
            devLog.setReqUrl(request.getRequestURI());
            devLog.setParamJson(CommonJoinPointUtil.getArgsJsonString(joinPoint));
            devLog.setOpTime(DateTime.now());
            devLog.setOpUser(userName);
            creatLogSignValue(devLog);
            devLogService.save(devLog);
        });
    }

    /**
     * 构建基础操作日志
     */
    private static DevLog genBasOpLog() {
        HttpServletRequest request = CommonServletUtil.getRequest();
        String ip = CommonIpAddressUtil.getIp(request);
        String loginId;
        try {
            loginId = StpUtil.getLoginIdAsString();
            if (ObjectUtil.isEmpty(loginId)) {
                loginId = "-1";
            }
        } catch (Exception e) {
            loginId = "-1";
        }
        DevLog devLog = new DevLog();
        devLog.setOpIp(CommonIpAddressUtil.getIp(request));
        devLog.setOpAddress(CommonIpAddressUtil.getCityInfo(ip));
        devLog.setOpBrowser(CommonUaUtil.getBrowser(request));
        devLog.setOpOs(CommonUaUtil.getOs(request));
        devLog.setCreateUser(loginId);
        return devLog;
    }
}

异步执行流程

复制代码
主线程                    异步线程(ThreadUtil)
  ↓                         ↓
执行业务方法           保存日志到数据库
  ↓                         ↓
返回响应              (后台执行,不影响响应时间)

优势

  • 接口响应时间不受日志写入影响
  • 提升系统吞吐量
  • 日志写入失败不影响主业务

2. 日志完整性签名保护

设计目的:防止日志数据被篡改,确保操作审计的可信度,满足等保2.0安全合规要求。

签名流程DevLogUtil.java):

java 复制代码
/**
 * 构建日志完整性保护签名数据
 */
private static void creatLogSignValue(DevLog devLog) {
    String logStr = devLog.toString().replaceAll(" +", "");
    devLog.setSignData(CommonCryptogramUtil.doSignature(logStr));
}

签名流程图

复制代码
日志对象 → toString()序列化 → 去除空格 → SM2签名 → 存入signData字段

技术要点

  • 序列化方式 :调用devLog.toString()获取对象字符串表示
  • 数据预处理 :使用replaceAll(" +", "")去除多余空格,保证签名一致性
  • 签名算法:国密SM2非对称签名算法
  • 签名存储 :将签名结果存入signData字段

2.1 签名算法实现

项目使用CommonCryptogramUtil工具类进行签名,底层基于sm-crypto库实现国密SM2算法:

java 复制代码
public class CommonCryptogramUtil {

    /** 公钥 */
    private static final String PUBLIC_KEY = "**********************";

    /** 私钥 */
    private static final String PRIVATE_KEY = "**********************";

    /**
     * 纯签名(使用私钥)
     */
    public static String doSignature(String str) {
        return Sm2.doSignature(str, PRIVATE_KEY);
    }

    /**
     * 验证签名结果(使用公钥)
     */
    public static boolean doVerifySignature(String originalStr, String str) {
        return Sm2.doVerifySignature(originalStr, str, PUBLIC_KEY);
    }
}

2.2 验签过程

验签流程

复制代码
获取日志记录 → 提取原始数据 → 重新序列化 → 去除空格 → SM2验签 → 返回验证结果

验签代码示例

java 复制代码
// 从数据库查询日志记录
DevLog devLog = devLogService.getById(logId);

// 获取签名数据
String signData = devLog.getSignData();

// 重新构建原始数据字符串
String logStr = devLog.toString().replaceAll(" +", "");

// 验证签名
boolean isValid = CommonCryptogramUtil.doVerifySignature(logStr, signData);

if (isValid) {
    // 签名验证通过,日志未被篡改
} else {
    // 签名验证失败,日志可能被篡改
}

2.3 算法特性
特性 说明
算法类型 非对称密码算法(国密SM2)
密钥长度 256位
签名方式 纯签名模式(不包含原文)
安全级别 等效于RSA-2048
适用场景 数据完整性保护、身份认证

2.4 应用价值
  • 防篡改:日志数据被篡改后签名验证会失败,可及时发现数据完整性问题
  • 审计可信:确保日志记录的真实性和完整性,满足审计要求
  • 合规要求:满足等保2.0中关于日志完整性保护的安全要求
  • 不可抵赖:基于非对称密钥体系,签名行为不可抵赖

注意事项

  1. 异步记录 :日志写入通过ThreadUtil.execute()异步执行,避免影响主业务性能
  2. 敏感信息:参数记录时需注意脱敏(如密码、手机号等)
  3. 性能影响:AOP拦截会增加少量开销,建议仅在关键业务方法上使用
  4. 日志完整性 :项目中通过creatLogSignValue()方法对日志内容进行签名保护
相关推荐
兵慌码乱5 小时前
基于Python+PyQt5+SQLite的药房管理系统实现:事务一致性与界面解耦全流程解析
python·sqlite·信号与槽·pyqt5·数据库设计·桌面应用开发·事务处理
朦胧之6 小时前
AI 编程-老项目改造篇
java·前端·后端
金銀銅鐵6 小时前
[Python] 体验用欧几里得算法计算最大公约数的过程
python·数学
swipe8 小时前
从 0 到 1 实现大文件上传:分片、秒传、断点续传、暂停、重试与服务端合并
前端·javascript·面试
爱勇宝8 小时前
我做了一个只用来搜歌词的小 App
android·前端·后端
甲维斯9 小时前
用AI还原《坦克大战》并3D化升级!
前端·人工智能·游戏开发
IT_陈寒9 小时前
SpringBoot自动配置坑了我一晚上,原来问题出在这
前端·人工智能·后端
FreakStudio10 小时前
W55MH32L-EVB 上手测评:硬件 TCP/IP 加持的以太网单片机,MicroPython 零门槛开发
python·单片机·嵌入式·大学生·面向对象·并行计算·电子diy·电子计算机
kyriewen10 小时前
AI 生成的代码能跑就行?这 5 个坑迟早炸
前端·javascript·ai编程
程序猿大帅10 小时前
别再只当调包侠了:用 Spring AI 落地 Function Calling,我被大模型硬生生砸出了三个大坑
java