196 lines
8.2 KiB
Java
196 lines
8.2 KiB
Java
package com.yunhai.bschclassmanage.aspect;
|
||
|
||
import com.alibaba.fastjson.JSON;
|
||
import com.yunhai.bschclassmanage.entity.SysOperationLog;
|
||
import com.yunhai.bschclassmanage.service.ISysOperationLogService;
|
||
import com.yunhai.bschclassmanage.utils.GzipUtil;
|
||
import com.yunhai.bschclassmanage.utils.OperationLogResolver;
|
||
import com.yunhai.bschclassmanage.utils.OtherUtil;
|
||
import com.yunhai.bschclassmanage.utils.SensitiveDataUtil;
|
||
import com.yunhai.bschclassmanage.utils.SyncLogRetryContext;
|
||
import com.yunhai.bschclassmanage.utils.UserContext;
|
||
import com.yunhai.platform.apistd.ApiResponse;
|
||
import com.yunhai.platform.utils.exception.DException;
|
||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||
import com.yunhai.platform.utils.exception.IErrorCode;
|
||
import com.yunhai.platform.utils.http.ApiResult;
|
||
import lombok.extern.slf4j.Slf4j;
|
||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||
import org.aspectj.lang.ProceedingJoinPoint;
|
||
import org.aspectj.lang.annotation.Around;
|
||
import org.aspectj.lang.annotation.Aspect;
|
||
import org.springframework.beans.factory.annotation.Autowired;
|
||
import org.springframework.core.annotation.Order;
|
||
import org.springframework.stereotype.Component;
|
||
import org.springframework.util.StringUtils;
|
||
import org.springframework.web.context.request.RequestContextHolder;
|
||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||
|
||
import javax.servlet.http.HttpServletRequest;
|
||
import javax.servlet.http.HttpServletResponse;
|
||
import javax.servlet.http.HttpSession;
|
||
import java.time.LocalDateTime;
|
||
import java.util.ArrayList;
|
||
import java.util.Arrays;
|
||
import java.util.List;
|
||
import java.util.Objects;
|
||
|
||
/**
|
||
* 系统审计日志切面(双轨之审计轨)
|
||
* - 全量拦截 controller 包所有方法,不依赖注解
|
||
* - 排除日志查询控制器自身(防递归)
|
||
* - 与业务日志切面 OperationLogAspect(Order 2) 物理隔离,互不影响
|
||
*/
|
||
@Slf4j
|
||
@Aspect
|
||
@Component
|
||
@Order(3)
|
||
public class SysOperationLogAspect {
|
||
|
||
/** 排除的控制器(日志查询自身,防递归刷屏) */
|
||
private static final List<String> EXCLUDE_CONTROLLERS = Arrays.asList(
|
||
"com.yunhai.bschclassmanage.controller.api.LogController", // 业务日志查询
|
||
"com.yunhai.bschclassmanage.controller.api.SysOperationLogController" // 审计日志查询
|
||
);
|
||
private static final int DEFAULT_MAX_RESPONSE_SIZE = 10240;
|
||
private static final int MAX_ERROR_MSG = 8000;
|
||
|
||
@Autowired
|
||
private ISysOperationLogService sysOperationLogService;
|
||
|
||
@Around("execution(* com.yunhai.bschclassmanage.controller..*.*(..))")
|
||
public Object around(ProceedingJoinPoint point) throws Throwable {
|
||
// 1. 排除日志控制器
|
||
String declType = point.getSignature().getDeclaringTypeName();
|
||
if (EXCLUDE_CONTROLLERS.contains(declType)) {
|
||
return point.proceed();
|
||
}
|
||
|
||
HttpServletRequest request = getRequest();
|
||
String httpMethod = request != null ? request.getMethod() : "";
|
||
|
||
long start = System.currentTimeMillis();
|
||
SysOperationLog entity = buildEntity(point, request, httpMethod);
|
||
|
||
Object result = null;
|
||
try {
|
||
result = point.proceed();
|
||
entity.setStatusCode(resolveSuccessCode(result));
|
||
fillResponse(entity, result, DEFAULT_MAX_RESPONSE_SIZE);
|
||
return result;
|
||
} catch (Throwable e) {
|
||
entity.setStatusCode(resolveErrorCode(e));
|
||
entity.setErrorMsg(buildErrorMsg(e));
|
||
throw e;
|
||
} finally {
|
||
entity.setCostTime(System.currentTimeMillis() - start);
|
||
sysOperationLogService.saveAsync(entity);
|
||
}
|
||
}
|
||
|
||
private SysOperationLog buildEntity(ProceedingJoinPoint point, HttpServletRequest request, String httpMethod) {
|
||
SysOperationLog e = new SysOperationLog();
|
||
e.setCreateTime(LocalDateTime.now());
|
||
|
||
// 用户信息(来自 UserContext,由 ControllerUserContextAspect Order1 已填充)
|
||
e.setUserId(UserContext.getUserId());
|
||
e.setUserName(StringUtils.hasText(UserContext.getUserName()) ? UserContext.getUserName() : "anonymous");
|
||
e.setSchId(UserContext.getSchId());
|
||
e.setRequestIp(StringUtils.hasText(UserContext.getIp()) ? UserContext.getIp() : OtherUtil.getClientIp());
|
||
|
||
String uri = request != null ? request.getRequestURI() : "";
|
||
String methodName = point.getSignature().getName();
|
||
e.setRequestUri(uri);
|
||
e.setRequestMethod(httpMethod);
|
||
e.setClassMethod(point.getTarget().getClass().getName() + "." + methodName);
|
||
e.setLogLevel(OperationLogResolver.resolveLevel(httpMethod, uri, methodName).getCode());
|
||
String opType = OperationLogResolver.resolveOperationType(httpMethod, uri, methodName);
|
||
if (SyncLogRetryContext.isRetry()) {
|
||
// 重试重放时,在操作类型上追加"(重试)"标记,便于与首次调用区分
|
||
opType = (opType == null ? "" : opType) + "(重试)";
|
||
}
|
||
e.setOperationType(opType);
|
||
if (request != null) {
|
||
e.setUserAgent(request.getHeader("User-Agent"));
|
||
e.setRequestParams(SensitiveDataUtil.mask(buildRequestParams(point, request)));
|
||
}
|
||
return e;
|
||
}
|
||
|
||
private String buildRequestParams(ProceedingJoinPoint point, HttpServletRequest request) {
|
||
try {
|
||
if (request != null && "GET".equalsIgnoreCase(request.getMethod())) {
|
||
String qs = request.getQueryString();
|
||
if (StringUtils.hasText(qs)) return qs;
|
||
}
|
||
Object[] args = point.getArgs();
|
||
if (args == null || args.length == 0) return "";
|
||
List<Object> filtered = new ArrayList<>();
|
||
for (Object arg : args) {
|
||
if (arg == null) continue;
|
||
if (arg instanceof HttpServletRequest || arg instanceof HttpServletResponse
|
||
|| arg instanceof HttpSession) continue;
|
||
filtered.add(arg);
|
||
}
|
||
return filtered.isEmpty() ? "" : JSON.toJSONString(filtered);
|
||
} catch (Exception ex) {
|
||
return "";
|
||
}
|
||
}
|
||
|
||
private void fillResponse(SysOperationLog e, Object result, int max) {
|
||
if (result == null) return;
|
||
try {
|
||
String resp = SensitiveDataUtil.mask(JSON.toJSONString(result));
|
||
if (resp.length() > max) resp = resp.substring(0, max) + "...[截断]";
|
||
e.setResponseData(GzipUtil.compress(resp));
|
||
} catch (Exception ex) {
|
||
log.warn("审计日志响应序列化失败, uri={}", e.getRequestUri(), ex);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 成功路径:提取全局返回对象的业务状态码。
|
||
* - Controller 直接返回 ApiResponse / ApiResult 时取其 code;
|
||
* - 其余(普通对象 / void / null)由平台 ApiResponseAdvice 统一包装为 success,code = SUCCESS(0)。
|
||
*/
|
||
private int resolveSuccessCode(Object result) {
|
||
if (result instanceof ApiResponse) {
|
||
return ((ApiResponse<?>) result).getCode();
|
||
}
|
||
if (result instanceof ApiResult) {
|
||
return ((ApiResult<?>) result).getCode();
|
||
}
|
||
return ErrorCode.SUCCESS.getCode();
|
||
}
|
||
|
||
/**
|
||
* 异常路径:从业务异常提取错误码,兜底 INTERNAL_SERVER_ERROR。
|
||
* DException.getCode() 返回 IErrorCode(可能为 null),需二次取 int code。
|
||
*/
|
||
private int resolveErrorCode(Throwable e) {
|
||
if (e instanceof DException) {
|
||
IErrorCode ec = ((DException) e).getCode();
|
||
if (ec != null) {
|
||
return ec.getCode();
|
||
}
|
||
}
|
||
return ErrorCode.INTERNAL_SERVER_ERROR.getCode();
|
||
}
|
||
|
||
private HttpServletRequest getRequest() {
|
||
ServletRequestAttributes a = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||
return a != null ? a.getRequest() : null;
|
||
}
|
||
|
||
private String buildErrorMsg(Throwable e) {
|
||
String msg = e.getMessage();
|
||
String head = (msg == null || msg.trim().isEmpty()) ? e.getClass().getName() : msg;
|
||
return truncate(head + System.lineSeparator() + ExceptionUtils.getStackTrace(e), MAX_ERROR_MSG);
|
||
}
|
||
|
||
private String truncate(String s, int max) {
|
||
return s != null && s.length() > max ? s.substring(0, max) : s;
|
||
}
|
||
}
|