优化
This commit is contained in:
@@ -14,6 +14,8 @@ public @interface BusinessAudit {
|
||||
String event();
|
||||
String resourceType();
|
||||
String resourceId() default ""; // SpEL, e.g. #request.orderId
|
||||
/** 业务数据所属客户 ID 的可选 SpEL;平台代客操作未配置时自动识别名为 tenantId 的参数。 */
|
||||
String tenantId() default "";
|
||||
String description() default "";
|
||||
boolean captureBefore() default true;
|
||||
boolean captureAfter() default true;
|
||||
|
||||
@@ -22,7 +22,6 @@ import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
@@ -57,9 +56,10 @@ public class BusinessAuditAspect {
|
||||
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),
|
||||
user == null ? headerLong(req, "X-Tenant-Id") : user.tenantId(),
|
||||
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(),
|
||||
@@ -81,9 +81,50 @@ public class BusinessAuditAspect {
|
||||
}
|
||||
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());
|
||||
Object v = parser.parseExpression(expression).getValue(ctx); return v == null ? null : v.toString();
|
||||
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; } }
|
||||
|
||||
@@ -30,7 +30,11 @@ public class SensitiveDataSanitizer {
|
||||
JsonNode root = mapper.valueToTree(value);
|
||||
redact(root);
|
||||
String json = mapper.writeValueAsString(root);
|
||||
return json.length() <= maxLength ? json : json.substring(0, maxLength) + "...[truncated]";
|
||||
if (json.length() <= maxLength) return json;
|
||||
ObjectNode truncated = mapper.createObjectNode();
|
||||
truncated.put("_truncated", true);
|
||||
truncated.put("_originalLength", json.length());
|
||||
return mapper.writeValueAsString(truncated);
|
||||
} catch (IllegalArgumentException | JsonProcessingException e) {
|
||||
return "\"[unserializable]\"";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.baiyue.common.audit;
|
||||
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class BusinessAuditAspectTest {
|
||||
@AfterEach
|
||||
void clearSecurityContext() {
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformOperationUsesTargetCustomerAndKeepsOperatorScope() throws Throwable {
|
||||
AuditEvent event = invoke(new AuthenticatedUser(7L, 0L, 0L, 1L,
|
||||
"platform.admin", "PLATFORM", "WEB", Set.of()), 10001L);
|
||||
|
||||
assertThat(event.tenantId()).isEqualTo(10001L);
|
||||
assertThat(event.operatorTenantId()).isZero();
|
||||
assertThat(event.userId()).isEqualTo(7L);
|
||||
assertThat(event.appCode()).isEqualTo("PLATFORM");
|
||||
}
|
||||
|
||||
@Test
|
||||
void merchantOperationIgnoresTargetCustomerArgument() throws Throwable {
|
||||
AuditEvent event = invoke(new AuthenticatedUser(8L, 10001L, 10001L, 2L,
|
||||
"merchant.user", "MERCHANT", "WEB", Set.of()), 20002L);
|
||||
|
||||
assertThat(event.tenantId()).isEqualTo(10001L);
|
||||
assertThat(event.operatorTenantId()).isEqualTo(10001L);
|
||||
}
|
||||
|
||||
private static AuditEvent invoke(AuthenticatedUser user, long requestedTenantId) throws Throwable {
|
||||
AuditSink sink = mock(AuditSink.class);
|
||||
BusinessAuditAspect aspect = new BusinessAuditAspect();
|
||||
ReflectionTestUtils.setField(aspect, "sink", sink);
|
||||
ReflectionTestUtils.setField(aspect, "sanitizer", new SensitiveDataSanitizer(new ObjectMapper(), 1024));
|
||||
ReflectionTestUtils.setField(aspect, "serviceName", "test-service");
|
||||
|
||||
SecurityContextHolder.getContext().setAuthentication(
|
||||
new UsernamePasswordAuthenticationToken(user, null, List.of()));
|
||||
|
||||
TestOperations target = new TestOperations();
|
||||
Method method = TestOperations.class.getDeclaredMethod("update", long.class);
|
||||
MethodSignature signature = mock(MethodSignature.class);
|
||||
when(signature.getMethod()).thenReturn(method);
|
||||
when(signature.getParameterNames()).thenReturn(null);
|
||||
|
||||
ProceedingJoinPoint joinPoint = mock(ProceedingJoinPoint.class);
|
||||
when(joinPoint.getSignature()).thenReturn(signature);
|
||||
when(joinPoint.getTarget()).thenReturn(target);
|
||||
when(joinPoint.getArgs()).thenReturn(new Object[]{requestedTenantId});
|
||||
when(joinPoint.proceed()).thenReturn("ok");
|
||||
|
||||
aspect.around(joinPoint, method.getAnnotation(BusinessAudit.class));
|
||||
|
||||
ArgumentCaptor<AuditEvent> event = ArgumentCaptor.forClass(AuditEvent.class);
|
||||
verify(sink).publish(event.capture());
|
||||
return event.getValue();
|
||||
}
|
||||
|
||||
private static final class TestOperations {
|
||||
@BusinessAudit(event = "TEST_UPDATE", resourceType = "TEST", captureBefore = false, captureAfter = false)
|
||||
public String update(long tenantId) {
|
||||
return "ok";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.baiyue.common.audit;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class SensitiveDataSanitizerTest {
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
void truncatedSnapshotRemainsValidJson() throws Exception {
|
||||
SensitiveDataSanitizer sanitizer = new SensitiveDataSanitizer(mapper, 40);
|
||||
|
||||
String sanitized = sanitizer.sanitize(Map.of("description", "x".repeat(200)));
|
||||
JsonNode parsed = mapper.readTree(sanitized);
|
||||
|
||||
assertThat(parsed.path("_truncated").asBoolean()).isTrue();
|
||||
assertThat(parsed.path("_originalLength").asInt()).isGreaterThan(40);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user