操作日志注解模块

功能说明

@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()方法对日志内容进行签名保护
相关推荐
CDN3601 小时前
【前端实战】LCP指标从2.5s优化至0.8s!用360CDN的WebP自适应与缓存策略榨干性能
前端·缓存
方也_arkling1 小时前
【Java-Day17】API篇-BigInteger和BigDecimal
java·开发语言
程序员三明治1 小时前
【AI】RAG 数据分块(Chunk)策略与实践
java·人工智能·后端·ai·大模型·llm·rag
EntyIU1 小时前
Tools使用指南
python·langchain
星辰_mya1 小时前
ThreadLocal之微服务链路追踪
java·开发语言·前端
MageGojo1 小时前
实时电影票房 API 接入实战:用 GET 请求获取影片票房榜单数据
python·电影票房·api 接口接入
松仔log1 小时前
Jetpack——DataStore
java·kotlin
咸鱼翻身小阿橙1 小时前
文件读写 + Qt Model/View + 自定义分页+搜索过滤
java·数据库·qt