package com.baiyue.common.audit; import jakarta.annotation.Resource; import com.baiyue.common.security.AuthenticatedUser; import com.baiyue.common.web.CorrelationContext; import jakarta.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Value; 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.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.expression.MethodBasedEvaluationContext; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import java.time.Instant; import java.util.UUID; /** * 业务审计切面,拦截审计注解方法并采集操作者、租户、请求、耗时、结果及脱敏前后快照。 */ @Aspect public class BusinessAuditAspect { private static final Logger log = LoggerFactory.getLogger(BusinessAuditAspect.class); @Resource private AuditSink sink; @Resource private SensitiveDataSanitizer sanitizer; @Value("${baiyue.audit.service-name:${spring.application.name:unknown-service}}") private String serviceName; private final ExpressionParser parser = new SpelExpressionParser(); @Around("@annotation(audit)") public Object around(ProceedingJoinPoint pjp, BusinessAudit audit) throws Throwable { long start = System.nanoTime(); String before = audit.captureBefore() ? sanitizer.sanitize(pjp.getArgs()) : null; Object result = null; Throwable failure = null; try { result = pjp.proceed(); return result; } catch (Throwable ex) { failure = ex; throw ex; } finally { try { publish(pjp, audit, before, result, failure, (System.nanoTime() - start) / 1_000_000); } catch (RuntimeException ex) { log.warn("Audit capture failed", ex); } } } private void publish(ProceedingJoinPoint pjp, BusinessAudit a, String before, Object result, Throwable failure, long duration) { HttpServletRequest req = request(); AuthenticatedUser user = currentUser(); String resourceId = evaluateResourceId(pjp, a.resourceId()); Long tenantId = resolveTenantId(pjp, a, user, req); sink.publish(new AuditEvent(UUID.randomUUID().toString(), a.event(), a.resourceType(), resourceId, a.description(), req == null ? null : CorrelationContext.requestId(req), req == null ? null : CorrelationContext.traceId(req), tenantId, user == null ? headerLong(req, "X-Operator-Tenant-Id") : user.operatorTenantId(), user == null ? null : user.userId(), user == null ? null : user.username(), user == null ? header(req, "X-App-Code") : user.appCode(), user == null ? null : user.clientType(), req == null ? null : req.getMethod(), req == null ? null : req.getRequestURI(), req == null ? null : clientIp(req), duration, failure == null, failure == null ? null : failure.getClass().getSimpleName(), failure == null ? null : failure.getMessage(), before, a.captureAfter() ? sanitizer.sanitize(result) : null, Instant.now(), serviceName)); } private HttpServletRequest request() { var attrs = RequestContextHolder.getRequestAttributes(); return attrs instanceof ServletRequestAttributes s ? s.getRequest() : null; } private AuthenticatedUser currentUser() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); return auth != null && auth.getPrincipal() instanceof AuthenticatedUser u ? u : null; } private String evaluateResourceId(ProceedingJoinPoint p, String expression) { if (expression == null || expression.isBlank()) return null; Object value = evaluate(p, expression); return value == null ? null : value.toString(); } private Long resolveTenantId(ProceedingJoinPoint p, BusinessAudit audit, AuthenticatedUser user, HttpServletRequest request) { if (user == null) return headerLong(request, "X-Tenant-Id"); if (!"PLATFORM".equals(user.appCode()) || !Long.valueOf(0L).equals(user.tenantId())) { return user.tenantId(); } Long explicit = evaluatePositiveLong(p, audit.tenantId()); if (explicit != null) return explicit; MethodSignature signature = (MethodSignature) p.getSignature(); String[] parameterNames = signature.getParameterNames(); if (parameterNames == null) { parameterNames = new DefaultParameterNameDiscoverer().getParameterNames(signature.getMethod()); } Object[] arguments = p.getArgs(); if (parameterNames != null) { for (int index = 0; index < parameterNames.length && index < arguments.length; index++) { if ("tenantId".equals(parameterNames[index])) { Long candidate = positiveLong(arguments[index]); if (candidate != null) return candidate; } } } return user.tenantId(); } private Long evaluatePositiveLong(ProceedingJoinPoint p, String expression) { if (expression == null || expression.isBlank()) return null; return positiveLong(evaluate(p, expression)); } private Object evaluate(ProceedingJoinPoint p, String expression) { MethodSignature s = (MethodSignature) p.getSignature(); var ctx = new MethodBasedEvaluationContext(p.getTarget(), s.getMethod(), p.getArgs(), new DefaultParameterNameDiscoverer()); return parser.parseExpression(expression).getValue(ctx); } private static Long positiveLong(Object value) { if (value == null) return null; try { long candidate = value instanceof Number number ? number.longValue() : Long.parseLong(value.toString()); return candidate > 0 ? candidate : null; } catch (NumberFormatException ignored) { return null; } } private static String header(HttpServletRequest r, String n) { return r == null ? null : r.getHeader(n); } private static Long headerLong(HttpServletRequest r, String n) { try { return header(r, n) == null ? null : Long.valueOf(header(r, n)); } catch (NumberFormatException e) { return null; } } private static String clientIp(HttpServletRequest r) { String x = r.getHeader("X-Forwarded-For"); return x == null ? r.getRemoteAddr() : x.split(",")[0].trim(); } }