功能说明
@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中关于日志完整性保护的安全要求
- 不可抵赖:基于非对称密钥体系,签名行为不可抵赖
注意事项
- 异步记录 :日志写入通过
ThreadUtil.execute()异步执行,避免影响主业务性能 - 敏感信息:参数记录时需注意脱敏(如密码、手机号等)
- 性能影响:AOP拦截会增加少量开销,建议仅在关键业务方法上使用
- 日志完整性 :项目中通过
creatLogSignValue()方法对日志内容进行签名保护