133 lines
6.8 KiB
Java
133 lines
6.8 KiB
Java
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(); }
|
|
}
|