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/**)调用日志切面。
* 拦截 {@code controller.oapi} 包下所有方法,规则:
* - POST 写操作:全量自动记录(无需注解),级别/类型由 OperationLogResolver 关键词判定;
* - GET 查询:默认不记录(高频调用,防刷爆表),满足任一条件则记录:方法标注 {@code @OapiLog}、或配置 oapi.log-get=true;
* - 排除 {@code SignKeyInternalController}(/oapi/internal,响应含 appSecret 明文,绝不落库);
* - 请求参数与响应均经 {@link SensitiveDataMasker} 脱敏后入库;
* - finally 中异步落库(有界池 oapiLogExecutor),失败不影响主业务。
* * @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,并映射应用名。
* 无 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 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); } 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; } }