添加推送日志功能
ci-userbase-service / build-and-push (push) Successful in 1m49s

This commit is contained in:
chenxing
2026-08-20 10:24:47 +08:00
parent 99414e2887
commit e001bc9d77
15 changed files with 1124 additions and 2 deletions
@@ -0,0 +1,241 @@
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;
}
}