fix: resolve critical and important issues in OperationLogAspect

- Fix blocking call blockOptional() in reactive context
- Fix double subscription issue in saveLogAsync
- Fix Chinese character encoding issues in log messages
- Add Flux support for reactive streams
- Add parameter and result size limits
- Add comprehensive JavaDoc for IpUtils
This commit is contained in:
张翔
2026-04-03 20:29:23 +08:00
parent 63c3f701a5
commit c4dc1d2ee1
2 changed files with 113 additions and 53 deletions
@@ -12,13 +12,16 @@ import org.slf4j.LoggerFactory;
import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerRequest;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
@Aspect @Aspect
@Component @Component
public class OperationLogAspect { public class OperationLogAspect {
private static final Logger logger = LoggerFactory.getLogger(OperationLogAspect.class); private static final Logger logger = LoggerFactory.getLogger(OperationLogAspect.class);
private static final int MAX_PARAM_LENGTH = 2000;
private static final int MAX_RESULT_LENGTH = 5000;
private final IOperationLogService logService; private final IOperationLogService logService;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
@@ -31,32 +34,50 @@ public class OperationLogAspect {
public Object around(ProceedingJoinPoint point, cn.novalon.manage.sys.audit.OperationLog operationLogAnnotation) throws Throwable { public Object around(ProceedingJoinPoint point, cn.novalon.manage.sys.audit.OperationLog operationLogAnnotation) throws Throwable {
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
ServerRequest serverRequest = extractServerRequest(point.getArgs()); ServerRequest serverRequest = extractServerRequest(point.getArgs());
String username = getCurrentUsername();
String ip = IpUtils.getClientIp(serverRequest); String ip = IpUtils.getClientIp(serverRequest);
String method = point.getSignature().toShortString(); String method = point.getSignature().toShortString();
String params = serializeParams(point.getArgs()); String params = serializeParams(point.getArgs());
Object result = null;
String status = "0";
String errorMsg = null;
try { try {
result = point.proceed(); Object result = point.proceed();
if (result instanceof Mono) { if (result instanceof Mono) {
return ((Mono<?>) result) return getCurrentUsername()
.doOnSuccess(res -> { .flatMap(username -> ((Mono<?>) result)
.flatMap(res -> {
long duration = System.currentTimeMillis() - startTime; long duration = System.currentTimeMillis() - startTime;
saveLogAsync(operationLogAnnotation, username, ip, method, params, res, duration, "0", null); return saveLogAsync(operationLogAnnotation, username, ip, method, params, res, duration, "0", null)
.thenReturn(res);
}) })
.doOnError(error -> { .onErrorResume(error -> {
long duration = System.currentTimeMillis() - startTime; long duration = System.currentTimeMillis() - startTime;
saveLogAsync(operationLogAnnotation, username, ip, method, params, null, duration, "1", error.getMessage()); return saveLogAsync(operationLogAnnotation, username, ip, method, params, null, duration, "1", error.getMessage())
}); .then(Mono.error(error));
})
);
} else if (result instanceof Flux) {
return getCurrentUsername()
.flatMapMany(username -> ((Flux<?>) result)
.collectList()
.flatMapMany(res -> {
long duration = System.currentTimeMillis() - startTime;
return saveLogAsync(operationLogAnnotation, username, ip, method, params, res, duration, "0", null)
.thenMany(Flux.fromIterable(res));
})
.onErrorResume(error -> {
long duration = System.currentTimeMillis() - startTime;
return saveLogAsync(operationLogAnnotation, username, ip, method, params, null, duration, "1", error.getMessage())
.thenMany(Flux.error(error));
})
);
} }
return result; return result;
} catch (Throwable error) { } catch (Throwable error) {
status = "1";
errorMsg = error.getMessage();
long duration = System.currentTimeMillis() - startTime; long duration = System.currentTimeMillis() - startTime;
saveLogAsync(operationLogAnnotation, username, ip, method, params, null, duration, status, errorMsg); getCurrentUsername()
.flatMap(username -> saveLogAsync(operationLogAnnotation, username, ip, method, params, null, duration, "1", error.getMessage()))
.subscribe();
throw error; throw error;
} }
} }
@@ -69,25 +90,24 @@ public class OperationLogAspect {
return null; return null;
} }
private String getCurrentUsername() { private Mono<String> getCurrentUsername() {
try {
return ReactiveSecurityContextHolder.getContext() return ReactiveSecurityContextHolder.getContext()
.map(ctx -> ctx.getAuthentication().getPrincipal()) .map(ctx -> ctx.getAuthentication().getPrincipal())
.cast(String.class) .map(principal -> principal instanceof String ? (String) principal : "system")
.blockOptional() .defaultIfEmpty("system")
.orElse("system"); .onErrorReturn("system");
} catch (Exception e) {
logger.warn("考试当前用命名失败: {}", e.getMessage());
return "system";
}
} }
private String serializeParams(Object[] args) { private String serializeParams(Object[] args) {
try { try {
if (args == null || args.length == 0) return null; if (args == null || args.length == 0) return null;
return objectMapper.writeValueAsString(args); String json = objectMapper.writeValueAsString(args);
if (json.length() > MAX_PARAM_LENGTH) {
return json.substring(0, MAX_PARAM_LENGTH) + "...(truncated)";
}
return json;
} catch (Exception e) { } catch (Exception e) {
logger.warn("处的包正数失败: {}", e.getMessage()); logger.warn("序列化参数失败: {}", e.getMessage());
return null; return null;
} }
} }
@@ -95,15 +115,21 @@ public class OperationLogAspect {
private String serializeResult(Object result) { private String serializeResult(Object result) {
try { try {
if (result == null) return null; if (result == null) return null;
return objectMapper.writeValueAsString(result); String json = objectMapper.writeValueAsString(result);
if (json.length() > MAX_RESULT_LENGTH) {
return json.substring(0, MAX_RESULT_LENGTH) + "...(truncated)";
}
return json;
} catch (Exception e) { } catch (Exception e) {
logger.warn("处皅包正数失败: {}", e.getMessage()); logger.warn("序列化结果失败: {}", e.getMessage());
return null; return null;
} }
} }
private void saveLogAsync(cn.novalon.manage.sys.audit.OperationLog annotation, String username, String ip, String method, String params, Object result, long duration, String status, String errorMsg) { private Mono<Void> saveLogAsync(cn.novalon.manage.sys.audit.OperationLog annotation,
Mono.fromRunnable(() -> { String username, String ip, String method,
String params, Object result, long duration,
String status, String errorMsg) {
OperationLog log = new OperationLog(); OperationLog log = new OperationLog();
log.setUsername(username); log.setUsername(username);
log.setOperation(annotation.module() + " - " + annotation.operation()); log.setOperation(annotation.module() + " - " + annotation.operation());
@@ -114,12 +140,11 @@ public class OperationLogAspect {
log.setDuration(duration); log.setDuration(duration);
log.setStatus(status); log.setStatus(status);
log.setErrorMsg(errorMsg); log.setErrorMsg(errorMsg);
logService.save(log)
.doOnSuccess(saved -> logger.debug("操作实跋信息成功退: {} - {}", annotation.module(), annotation.operation())) return logService.save(log)
.doOnError(error -> logger.error("꓍作实跋信息成功退: {}", error.getMessage())) .doOnSuccess(saved -> logger.debug("操作日志保存成功: {} - {}",
.subscribe(); annotation.module(), annotation.operation()))
}) .doOnError(error -> logger.error("操作日志保存失败: {}", error.getMessage()))
.subscribeOn(Schedulers.boundedElastic()) .then();
.subscribe();
} }
} }
@@ -4,30 +4,55 @@ import org.springframework.web.reactive.function.server.ServerRequest;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.Optional; import java.util.Optional;
/**
* IP地址工具类
* 用于从ServerRequest中获取客户端真实IP地址
* 支持代理服务器场景(X-Forwarded-For, X-Real-IP)
*
* @author 张翔
* @date 2026-04-03
*/
public class IpUtils { public class IpUtils {
private static final String UNKNOWN = "unknown"; private static final String UNKNOWN = "unknown";
private static final String LOCALHOST_IP = "127.0.0.1"; private static final String LOCALHOST_IP = "127.0.0.1";
private static final String LOCALHOST_IPV6 = "0:0:0:0:0:0:0:1"; private static final String LOCALHOST_IPV6 = "0:0:0:0:0:0:0:1";
/**
* 从ServerRequest中获取客户端真实IP地址
* 支持代理服务器场景,优先级: X-Forwarded-For > X-Real-IP > RemoteAddress
*
* @param request ServerRequest对象
* @return 客户端IP地址,获取失败返回"unknown"
*/
public static String getClientIp(ServerRequest request) { public static String getClientIp(ServerRequest request) {
if (request == null) { if (request == null) {
return UNKNOWN; return UNKNOWN;
} }
String ip = getXForwardedForIp(request); String ip = getXForwardedForIp(request);
if (isValidIp(ip)) { if (isValidIp(ip)) {
return ip; return ip;
} }
ip = getXRealIp(request); ip = getXRealIp(request);
if (isValidIp(ip)) { if (isValidIp(ip)) {
return ip; return ip;
} }
ip = getRemoteAddress(request); ip = getRemoteAddress(request);
if (isValidIp(ip)) { if (isValidIp(ip)) {
return ip; return ip;
} }
return UNKNOWN; return UNKNOWN;
} }
/**
* 从X-Forwarded-For头获取IP地址
* X-Forwarded-For格式: client, proxy1, proxy2
* 取第一个非unknown的有效IP
*/
private static String getXForwardedForIp(ServerRequest request) { private static String getXForwardedForIp(ServerRequest request) {
String ip = request.headers().firstHeader("X-Forwarded-For"); String ip = request.headers().firstHeader("X-Forwarded-For");
if (ip != null && ip.length() > 0 && !UNKNOWN.equalsIgnoreCase(ip)) { if (ip != null && ip.length() > 0 && !UNKNOWN.equalsIgnoreCase(ip)) {
@@ -40,6 +65,9 @@ public class IpUtils {
return null; return null;
} }
/**
* 从X-Real-IP头获取IP地址
*/
private static String getXRealIp(ServerRequest request) { private static String getXRealIp(ServerRequest request) {
String ip = request.headers().firstHeader("X-Real-IP"); String ip = request.headers().firstHeader("X-Real-IP");
if (ip != null && ip.length() > 0 && !UNKNOWN.equalsIgnoreCase(ip)) { if (ip != null && ip.length() > 0 && !UNKNOWN.equalsIgnoreCase(ip)) {
@@ -48,6 +76,10 @@ public class IpUtils {
return null; return null;
} }
/**
* 从RemoteAddress获取IP地址
* 将IPv6本地地址转换为IPv4格式
*/
private static String getRemoteAddress(ServerRequest request) { private static String getRemoteAddress(ServerRequest request) {
Optional<InetSocketAddress> remoteAddress = request.remoteAddress(); Optional<InetSocketAddress> remoteAddress = request.remoteAddress();
if (remoteAddress.isPresent()) { if (remoteAddress.isPresent()) {
@@ -60,6 +92,9 @@ public class IpUtils {
return null; return null;
} }
/**
* 验证IP地址是否有效
*/
private static boolean isValidIp(String ip) { private static boolean isValidIp(String ip) {
return ip != null && ip.length() > 0 && !UNKNOWN.equalsIgnoreCase(ip); return ip != null && ip.length() > 0 && !UNKNOWN.equalsIgnoreCase(ip);
} }