242 lines
9.7 KiB
Java
242 lines
9.7 KiB
Java
package com.yunhai.userbase.aspect;
|
||
|
||
import com.alibaba.fastjson.JSON;
|
||
import com.yunhai.platform.appauth.AppAuthFilter;
|
||
import com.yunhai.userbase.common.annotation.OapiLog;
|
||
import com.yunhai.userbase.entity.ApplicationInfo;
|
||
import com.yunhai.userbase.entity.SysOapiLog;
|
||
import com.yunhai.userbase.repository.ApplicationInfoRepository;
|
||
import com.yunhai.userbase.service.ISysOapiLogService;
|
||
import com.yunhai.userbase.utils.GzipUtil;
|
||
import com.yunhai.userbase.utils.IpUtils;
|
||
import com.yunhai.userbase.utils.OperationLogResolver;
|
||
import com.yunhai.userbase.utils.SensitiveDataMasker;
|
||
import lombok.extern.slf4j.Slf4j;
|
||
import org.apache.commons.lang3.StringUtils;
|
||
import org.aspectj.lang.ProceedingJoinPoint;
|
||
import org.aspectj.lang.annotation.Around;
|
||
import org.aspectj.lang.annotation.Aspect;
|
||
import org.aspectj.lang.reflect.MethodSignature;
|
||
import org.springframework.beans.factory.annotation.Autowired;
|
||
import org.springframework.beans.factory.annotation.Value;
|
||
import org.springframework.stereotype.Component;
|
||
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.lang.reflect.Method;
|
||
import java.util.ArrayList;
|
||
import java.util.List;
|
||
|
||
/**
|
||
* 开放接口(/oapi/**)调用日志切面。<br>
|
||
* 拦截 {@code controller.oapi} 包下所有方法,规则:<br>
|
||
* - POST 写操作:全量自动记录(无需注解),级别/类型由 OperationLogResolver 关键词判定;<br>
|
||
* - GET 查询:默认不记录(高频调用,防刷爆表),满足任一条件则记录:方法标注 {@code @OapiLog}、或配置 oapi.log-get=true;<br>
|
||
* - 排除 {@code SignKeyInternalController}(/oapi/internal,响应含 appSecret 明文,绝不落库);<br>
|
||
* - 请求参数与响应均经 {@link SensitiveDataMasker} 脱敏后入库;<br>
|
||
* - finally 中异步落库(有界池 oapiLogExecutor),失败不影响主业务。<br>
|
||
*
|
||
* @author chenx
|
||
* @date 2026/8/20
|
||
*/
|
||
@Slf4j
|
||
@Aspect
|
||
@Component
|
||
public class OapiLogAspect {
|
||
|
||
/** 内部密钥端点控制器,排除(响应含 appSecret 明文)。 */
|
||
private static final String INTERNAL_CONTROLLER = "com.yunhai.userbase.controller.oapi.SignKeyInternalController";
|
||
|
||
/** GET 默认不记录,开启本配置后全量记录。 */
|
||
@Value("${oapi.log-get:false}")
|
||
private boolean logGet;
|
||
|
||
@Autowired
|
||
private ISysOapiLogService sysOapiLogService;
|
||
|
||
@Autowired
|
||
private ApplicationInfoRepository applicationInfoRepository;
|
||
|
||
@Around("execution(* com.yunhai.userbase.controller.oapi..*.*(..))")
|
||
public Object around(ProceedingJoinPoint point) throws Throwable {
|
||
if (INTERNAL_CONTROLLER.equals(point.getSignature().getDeclaringTypeName())) {
|
||
return point.proceed();
|
||
}
|
||
|
||
HttpServletRequest request = getRequest();
|
||
String httpMethod = request != null ? request.getMethod() : "";
|
||
boolean isGet = "GET".equalsIgnoreCase(httpMethod);
|
||
|
||
OapiLog oapiLog = findAnnotation(point);
|
||
// 决策:POST 全记;GET 仅当注解强制或配置开启才记
|
||
if (isGet && oapiLog == null && !logGet) {
|
||
return point.proceed();
|
||
}
|
||
|
||
long start = System.currentTimeMillis();
|
||
SysOapiLog entity = buildEntity(point, request, httpMethod, oapiLog);
|
||
|
||
boolean saveResponse = oapiLog != null ? oapiLog.saveResponse() : true;
|
||
int maxResponseSize = oapiLog != null ? oapiLog.maxResponseSize() : 10240;
|
||
|
||
Object result = null;
|
||
try {
|
||
result = point.proceed();
|
||
entity.setStatusCode(200);
|
||
fillResponse(entity, result, saveResponse, maxResponseSize);
|
||
} catch (Throwable e) {
|
||
entity.setStatusCode(500);
|
||
entity.setErrorMsg(truncate(e.getMessage(), 2000));
|
||
throw e;
|
||
} finally {
|
||
entity.setCostTime(System.currentTimeMillis() - start);
|
||
sysOapiLogService.saveAsync(entity);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
private OapiLog findAnnotation(ProceedingJoinPoint point) {
|
||
try {
|
||
MethodSignature signature = (MethodSignature) point.getSignature();
|
||
Method method = signature.getMethod();
|
||
return method.getAnnotation(OapiLog.class);
|
||
} catch (Exception e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
private SysOapiLog buildEntity(ProceedingJoinPoint point, HttpServletRequest request,
|
||
String httpMethod, OapiLog oapiLog) {
|
||
SysOapiLog entity = new SysOapiLog();
|
||
|
||
// 调用方应用:验签 attribute → appKey → application_info 解析应用名(失败记 appKey 原值,不阻塞)
|
||
resolveCallerApp(request, entity);
|
||
|
||
String requestUri = "";
|
||
if (request != null) {
|
||
entity.setRequestIp(IpUtils.getIpAddr(request));
|
||
requestUri = request.getRequestURI();
|
||
entity.setUserAgent(request.getHeader("User-Agent"));
|
||
entity.setRequestParams(maskRequestParams(point, request));
|
||
} else {
|
||
entity.setRequestParams(maskRequestParams(point, null));
|
||
}
|
||
entity.setRequestUri(requestUri);
|
||
entity.setRequestMethod(httpMethod);
|
||
|
||
String methodName = point.getSignature().getName();
|
||
entity.setLogLevel(OperationLogResolver.resolveLevel(httpMethod, requestUri, methodName).getCode());
|
||
entity.setOperationType(OperationLogResolver.resolveOperationType(httpMethod, requestUri, methodName));
|
||
entity.setClassMethod(point.getTarget().getClass().getName() + "." + methodName);
|
||
if (oapiLog != null && StringUtils.isNotBlank(oapiLog.value())) {
|
||
entity.setOperationDesc(oapiLog.value());
|
||
}
|
||
return entity;
|
||
}
|
||
|
||
/**
|
||
* 从 request attribute 解析调用方 appKey,并映射应用名。<br>
|
||
* 无 attribute / 解析失败:appKey 记 0、应用名记 unknown,仅 warn,不抛异常。
|
||
*/
|
||
private void resolveCallerApp(HttpServletRequest request, SysOapiLog entity) {
|
||
entity.setCallerAppKey(0);
|
||
entity.setCallerAppName("unknown");
|
||
if (request == null) {
|
||
return;
|
||
}
|
||
Object attr = request.getAttribute(AppAuthFilter.APP_AUTH_ATTR);
|
||
if (attr == null) {
|
||
log.warn("[oapi-log] no AppAuth attr (should be blocked by RequireAppSignFilter), uri={}",
|
||
request.getRequestURI());
|
||
return;
|
||
}
|
||
String appKeyStr = (attr instanceof String) ? (String) attr : String.valueOf(attr);
|
||
if (StringUtils.isBlank(appKeyStr)) {
|
||
log.warn("[oapi-log] AppAuth attr is blank, uri={}", request.getRequestURI());
|
||
return;
|
||
}
|
||
Integer appKey;
|
||
try {
|
||
appKey = Integer.parseInt(appKeyStr.trim());
|
||
} catch (NumberFormatException e) {
|
||
log.warn("[oapi-log] invalid appKey format={}, uri={}", appKeyStr, request.getRequestURI());
|
||
return;
|
||
}
|
||
entity.setCallerAppKey(appKey);
|
||
ApplicationInfo app = applicationInfoRepository.findByAppKey(appKey);
|
||
entity.setCallerAppName(app != null && StringUtils.isNotBlank(app.getName())
|
||
? app.getName() : String.valueOf(appKey));
|
||
}
|
||
|
||
/**
|
||
* 构建请求参数:GET 优先 queryString;其余序列化方法入参(过滤 Servlet 三大件),统一过脱敏。
|
||
*/
|
||
private String maskRequestParams(ProceedingJoinPoint point, HttpServletRequest request) {
|
||
try {
|
||
if (request != null && "GET".equalsIgnoreCase(request.getMethod())) {
|
||
String qs = request.getQueryString();
|
||
if (StringUtils.isNotBlank(qs)) {
|
||
return SensitiveDataMasker.maskJson(qs);
|
||
}
|
||
}
|
||
Object[] args = point.getArgs();
|
||
if (args == null || args.length == 0) {
|
||
return "";
|
||
}
|
||
List<Object> filtered = new ArrayList<Object>();
|
||
for (Object arg : args) {
|
||
if (arg == null) {
|
||
continue;
|
||
}
|
||
if (arg instanceof HttpServletRequest || arg instanceof HttpServletResponse
|
||
|| arg instanceof HttpSession) {
|
||
continue;
|
||
}
|
||
filtered.add(arg);
|
||
}
|
||
if (filtered.isEmpty()) {
|
||
return "";
|
||
}
|
||
return SensitiveDataMasker.maskObject(filtered);
|
||
} catch (Exception e) {
|
||
return "";
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 写入响应结果(脱敏 → 截断 → GZIP)。
|
||
*/
|
||
private void fillResponse(SysOapiLog entity, Object result, boolean saveResponse, int maxResponseSize) {
|
||
if (!saveResponse || result == null) {
|
||
return;
|
||
}
|
||
try {
|
||
String resp = SensitiveDataMasker.maskObject(result);
|
||
if (resp == null) {
|
||
return;
|
||
}
|
||
if (resp.length() > maxResponseSize) {
|
||
resp = resp.substring(0, maxResponseSize) + "...[截断]";
|
||
}
|
||
entity.setResponseData(GzipUtil.compress(resp));
|
||
} catch (Exception e) {
|
||
log.warn("开放接口日志响应序列化失败, uri={}", entity.getRequestUri(), e);
|
||
}
|
||
}
|
||
|
||
private HttpServletRequest getRequest() {
|
||
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||
return attrs != null ? attrs.getRequest() : null;
|
||
}
|
||
|
||
private String truncate(String text, int maxLen) {
|
||
if (text == null) {
|
||
return null;
|
||
}
|
||
return text.length() > maxLen ? text.substring(0, maxLen) : text;
|
||
}
|
||
}
|