feat: enhance coding harness, model security and system features

This commit is contained in:
ageerle
2026-09-08 23:33:29 +08:00
parent d4ed2b8672
commit bcff4fd9e3
259 changed files with 15183 additions and 2713 deletions

View File

@@ -52,6 +52,12 @@
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -5,16 +5,14 @@ import cn.hutool.http.HttpStatus;
import com.fasterxml.jackson.core.JsonParseException;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.core.domain.R;
import org.ruoyi.common.core.exception.ServiceException;
import org.ruoyi.common.core.exception.SseException;
import org.ruoyi.common.core.exception.base.BaseException;
import org.ruoyi.common.core.utils.StreamUtils;
import org.ruoyi.common.json.utils.JsonUtils;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.ruoyi.common.web.utils.SafeRequestLogUtils;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
@@ -38,15 +36,18 @@ import java.io.IOException;
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final String SAFE_SERVICE_ERROR_MESSAGE = "请求处理失败";
private static final String SAFE_VALIDATION_ERROR_MESSAGE = "请求参数校验失败";
/**
* 请求方式不支持
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public R<Void> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e,
HttpServletRequest request) {
String requestURI = request.getRequestURI();
String requestURI = safeUri(request);
log.error("请求地址'{}',不支持'{}'请求", requestURI, e.getMethod());
return R.fail(HttpStatus.HTTP_BAD_METHOD, e.getMessage());
return R.fail(HttpStatus.HTTP_BAD_METHOD, "请求方式不支持");
}
/**
@@ -54,9 +55,11 @@ public class GlobalExceptionHandler {
*/
@ExceptionHandler(ServiceException.class)
public R<Void> handleServiceException(ServiceException e, HttpServletRequest request) {
log.error(e.getMessage());
log.error("request_failure category=SERVICE_EXCEPTION errorType={}", e.getClass().getName());
Integer code = e.getCode();
return ObjectUtil.isNotNull(code) ? R.fail(code, e.getMessage()) : R.fail(e.getMessage());
return ObjectUtil.isNotNull(code)
? R.fail(code, SAFE_SERVICE_ERROR_MESSAGE)
: R.fail(SAFE_SERVICE_ERROR_MESSAGE);
}
/**
@@ -65,8 +68,8 @@ public class GlobalExceptionHandler {
@ResponseStatus(org.springframework.http.HttpStatus.UNAUTHORIZED)
@ExceptionHandler(SseException.class)
public String handleNotLoginException(SseException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.debug("请求地址'{}',认证失败'{}',无法访问系统资源", requestURI, e.getMessage());
String requestURI = safeUri(request);
log.debug("请求地址'{}',认证失败,无法访问系统资源", requestURI);
return JsonUtils.toJsonString(R.fail(HttpStatus.HTTP_UNAUTHORIZED, "认证失败,无法访问系统资源"));
}
@@ -75,9 +78,9 @@ public class GlobalExceptionHandler {
*/
@ExceptionHandler(ServletException.class)
public R<Void> handleServletException(ServletException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',发生未知异常.", requestURI, e);
return R.fail(e.getMessage());
String requestURI = safeUri(request);
log.error("请求地址'{}',发生 Servlet 异常: {}", requestURI, e.getClass().getSimpleName());
return internalError();
}
/**
@@ -85,8 +88,8 @@ public class GlobalExceptionHandler {
*/
@ExceptionHandler(BaseException.class)
public R<Void> handleBaseException(BaseException e, HttpServletRequest request) {
log.error(e.getMessage());
return R.fail(e.getMessage());
log.error("request_failure category=BASE_EXCEPTION errorType={}", e.getClass().getName());
return R.fail(SAFE_SERVICE_ERROR_MESSAGE);
}
/**
@@ -94,9 +97,9 @@ public class GlobalExceptionHandler {
*/
@ExceptionHandler(MissingPathVariableException.class)
public R<Void> handleMissingPathVariableException(MissingPathVariableException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
String requestURI = safeUri(request);
log.error("请求路径中缺少必需的路径变量'{}',发生系统异常.", requestURI);
return R.fail(String.format("请求路径中缺少必需的路径变量[%s]", e.getVariableName()));
return R.fail("请求路径参数缺失");
}
/**
@@ -104,9 +107,9 @@ public class GlobalExceptionHandler {
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public R<Void> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
String requestURI = safeUri(request);
log.error("请求参数类型不匹配'{}',发生系统异常.", requestURI);
return R.fail(String.format("请求参数类型不匹配,参数[%s]要求类型为:'%s',但输入值为:'%s'", e.getName(), e.getRequiredType().getName(), e.getValue()));
return R.fail("请求参数类型不匹配");
}
/**
@@ -114,9 +117,9 @@ public class GlobalExceptionHandler {
*/
@ExceptionHandler(NoHandlerFoundException.class)
public R<Void> handleNoHandlerFoundException(NoHandlerFoundException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
String requestURI = safeUri(request);
log.error("请求地址'{}'不存在.", requestURI);
return R.fail(HttpStatus.HTTP_NOT_FOUND, e.getMessage());
return R.fail(HttpStatus.HTTP_NOT_FOUND, "请求地址不存在");
}
/**
@@ -126,7 +129,7 @@ public class GlobalExceptionHandler {
*/
@ExceptionHandler(IOException.class)
public R<Void> handleIoException(IOException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
String requestURI = safeUri(request);
if (requestURI.contains("sse")) {
// sse 经常性连接中断 例如关闭浏览器 直接屏蔽
return null;
@@ -134,10 +137,12 @@ public class GlobalExceptionHandler {
// 排除文件下载/导出相关的 IOException,让异常正常传播以便上层处理
if (requestURI.contains("/export") || requestURI.contains("/download")) {
// 重新抛出,让调用方处理
throw new RuntimeException("文件导出/下载IO异常: " + e.getMessage(), e);
log.error("请求地址'{}',文件导出/下载连接中断: {}", requestURI,
e.getClass().getSimpleName());
return internalError();
}
log.error("请求地址'{}',连接中断", requestURI, e);
return R.fail(e.getMessage());
log.error("请求地址'{}',连接中断: {}", requestURI, e.getClass().getSimpleName());
return internalError();
}
/**
@@ -152,16 +157,16 @@ public class GlobalExceptionHandler {
*/
@ExceptionHandler(RuntimeException.class)
public R<Void> handleRuntimeException(RuntimeException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
String requestURI = safeUri(request);
// 对于文件导出相关异常,不进行封装处理,让原始异常信息传播
Throwable cause = e.getCause();
if (requestURI.contains("/export") || requestURI.contains("/download")) {
log.error("请求地址'{}',文件导出/下载异常.", requestURI, e);
// 对于文件导出,直接返回异常信息,不进行额外封装
return R.fail(cause != null ? cause.getMessage() : e.getMessage());
log.error("请求地址'{}',文件导出/下载异常: {}", requestURI,
cause == null ? e.getClass().getSimpleName() : cause.getClass().getSimpleName());
return internalError();
}
log.error("请求地址'{}',发生未知异常.", requestURI, e);
return R.fail(e.getMessage());
log.error("请求地址'{}',发生未知异常: {}", requestURI, e.getClass().getSimpleName());
return internalError();
}
/**
@@ -169,9 +174,9 @@ public class GlobalExceptionHandler {
*/
@ExceptionHandler(Exception.class)
public R<Void> handleException(Exception e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',发生系统异常.", requestURI, e);
return R.fail(e.getMessage());
String requestURI = safeUri(request);
log.error("请求地址'{}',发生系统异常: {}", requestURI, e.getClass().getSimpleName());
return internalError();
}
/**
@@ -179,9 +184,8 @@ public class GlobalExceptionHandler {
*/
@ExceptionHandler(BindException.class)
public R<Void> handleBindException(BindException e) {
log.error(e.getMessage());
String message = StreamUtils.join(e.getAllErrors(), DefaultMessageSourceResolvable::getDefaultMessage, ", ");
return R.fail(message);
log.error("请求绑定失败: {}", e.getClass().getSimpleName());
return R.fail(SAFE_VALIDATION_ERROR_MESSAGE);
}
/**
@@ -189,9 +193,8 @@ public class GlobalExceptionHandler {
*/
@ExceptionHandler(ConstraintViolationException.class)
public R<Void> constraintViolationException(ConstraintViolationException e) {
log.error(e.getMessage());
String message = StreamUtils.join(e.getConstraintViolations(), ConstraintViolation::getMessage, ", ");
return R.fail(message);
log.error("约束校验失败: {}", e.getClass().getSimpleName());
return R.fail(SAFE_VALIDATION_ERROR_MESSAGE);
}
/**
@@ -199,9 +202,8 @@ public class GlobalExceptionHandler {
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public R<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
log.error(e.getMessage());
String message = StreamUtils.join(e.getBindingResult().getAllErrors(), DefaultMessageSourceResolvable::getDefaultMessage, ", ");
return R.fail(message);
log.error("请求参数校验失败: {}", e.getClass().getSimpleName());
return R.fail(SAFE_VALIDATION_ERROR_MESSAGE);
}
/**
@@ -210,9 +212,9 @@ public class GlobalExceptionHandler {
*/
@ExceptionHandler(JsonParseException.class)
public R<Void> handleJsonParseException(JsonParseException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}' 发生 JSON 解析异常: {}", requestURI, e.getMessage());
return R.fail(HttpStatus.HTTP_BAD_REQUEST, "请求数据格式错误(JSON 解析失败):" + e.getMessage());
String requestURI = safeUri(request);
log.error("请求地址'{}'发生 JSON 解析异常: {}", requestURI, e.getClass().getSimpleName());
return R.fail(HttpStatus.HTTP_BAD_REQUEST, "请求数据格式错误(JSON 解析失败)");
}
/**
@@ -220,8 +222,16 @@ public class GlobalExceptionHandler {
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public R<Void> handleHttpMessageNotReadableException(HttpMessageNotReadableException e, HttpServletRequest request) {
log.error("请求地址'{}', 参数解析失败: {}", request.getRequestURI(), e.getMessage());
return R.fail(HttpStatus.HTTP_BAD_REQUEST, "请求参数格式错误:" + e.getMostSpecificCause().getMessage());
log.error("请求地址'{}',参数解析失败: {}", safeUri(request), e.getClass().getSimpleName());
return R.fail(HttpStatus.HTTP_BAD_REQUEST, "请求参数格式错误");
}
private String safeUri(HttpServletRequest request) {
return SafeRequestLogUtils.sanitizeUri(request.getRequestURI());
}
private R<Void> internalError() {
return R.fail("系统异常,请联系管理员");
}
}

View File

@@ -1,24 +1,16 @@
package org.ruoyi.common.web.interceptor;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.StopWatch;
import org.ruoyi.common.core.utils.StringUtils;
import org.ruoyi.common.json.utils.JsonUtils;
import org.ruoyi.common.web.filter.RepeatedlyRequestWrapper;
import org.springframework.http.MediaType;
import org.ruoyi.common.web.utils.SafeRequestLogUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import java.io.BufferedReader;
import java.util.Map;
/**
* web的调用时间统计拦截器
* Web request timing interceptor. Request payloads, form fields and query strings are
* deliberately outside this logging boundary.
*
* @author Lion Li
* @since 3.3.0
@@ -26,64 +18,44 @@ import java.util.Map;
@Slf4j
public class PlusWebInvokeTimeInterceptor implements HandlerInterceptor {
private final static ThreadLocal<StopWatch> KEY_CACHE = new ThreadLocal<>();
private static final ThreadLocal<StopWatch> KEY_CACHE = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String url = request.getMethod() + " " + request.getRequestURI();
// 打印请求参数
if (isJsonRequest(request)) {
String jsonParam = "";
if (request instanceof RepeatedlyRequestWrapper) {
BufferedReader reader = request.getReader();
jsonParam = IoUtil.read(reader);
}
log.info("[PLUS]开始请求 => URL[{}],参数类型[json],参数:[{}]", url, jsonParam);
} else {
Map<String, String[]> parameterMap = request.getParameterMap();
if (MapUtil.isNotEmpty(parameterMap)) {
String parameters = JsonUtils.toJsonString(parameterMap);
log.info("[PLUS]开始请求 => URL[{}],参数类型[param],参数:[{}]", url, parameters);
} else {
log.info("[PLUS]开始请求 => URL[{}],无参数", url);
}
}
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
StopWatch stopWatch = new StopWatch();
KEY_CACHE.set(stopWatch);
stopWatch.start();
log.info("[PLUS] request_started method={} path={}", request.getMethod(), safePath(request));
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) {
// Completion metadata is emitted once from afterCompletion.
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) {
StopWatch stopWatch = KEY_CACHE.get();
if (ObjectUtil.isNotNull(stopWatch)) {
stopWatch.stop();
log.info("[PLUS]结束请求 => URL[{}],耗时:[{}]毫秒", request.getMethod() + " " + request.getRequestURI(), stopWatch.getDuration().toMillis());
try {
if (stopWatch == null) {
return;
}
if (stopWatch.isStarted() && !stopWatch.isStopped()) {
stopWatch.stop();
}
log.info("[PLUS] request_completed method={} path={} status={} elapsedMs={} outcome={} errorType={}",
request.getMethod(), safePath(request), response.getStatus(),
stopWatch.getDuration().toMillis(), ex == null ? "SUCCESS" : "FAILED",
ex == null ? "none" : ex.getClass().getName());
} finally {
KEY_CACHE.remove();
}
}
/**
* 判断本次请求的数据类型是否为json
*
* @param request request
* @return boolean
*/
private boolean isJsonRequest(HttpServletRequest request) {
String contentType = request.getContentType();
if (contentType != null) {
return StringUtils.startsWithIgnoreCase(contentType, MediaType.APPLICATION_JSON_VALUE);
}
return false;
private static String safePath(HttpServletRequest request) {
return SafeRequestLogUtils.sanitizeUri(request.getRequestURI());
}
}

View File

@@ -0,0 +1,263 @@
package org.ruoyi.common.web.utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.ruoyi.common.core.utils.StringUtils;
import java.lang.reflect.Array;
import java.time.temporal.TemporalAccessor;
import java.util.ArrayList;
import java.util.Date;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
/**
* Fail-closed boundary for values that may be written to HTTP logs.
*/
public final class SafeRequestLogUtils {
public static final String REDACTED = "[REDACTED]";
private static final int MAX_URI_LENGTH = 512;
private static final int MAX_TEXT_LENGTH = 4096;
private static final int MAX_DEPTH = 16;
private static final int MAX_COLLECTION_ITEMS = 100;
private static final String TRUNCATED = "[TRUNCATED]";
private static final String CYCLE = "[CYCLE]";
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final Set<String> SECRET_KEYS = Set.of(
"token", "tokenid", "accesstoken", "refreshtoken", "authorization",
"apikey", "clientsecret", "password", "passwd", "pwd", "secret",
"secretkey", "secretaccesskey", "sessiontoken", "jwtsecretkey",
"signingkey", "signature", "privatekey", "credential", "credentials"
);
private static final Pattern ONLINE_TOKEN_PATH = Pattern.compile(
"(?i)(/monitor/online/(?:myself/)?)([^/?#;\\s]+)");
private static final Pattern MYSELF_TOKEN_PATH = Pattern.compile(
"(?i)(/myself/)([^/?#;\\s]+)");
private static final Pattern GENERIC_SECRET_PATH = Pattern.compile(
"(?i)(/(?:token(?:[-_]?id)?|access[-_]?token|refresh[-_]?token|api[-_]?key)/)"
+ "([^/?#;\\s]+)");
private static final String SECRET_FIELD =
"(?:[a-z0-9_-]*(?:token|api[_-]?key|secret|password|passwd|pwd|credential|"
+ "authorization|signing[_-]?key|signature)[a-z0-9_-]*)";
private static final Pattern JSON_SECRET = Pattern.compile(
"(?i)(\\\"" + SECRET_FIELD + "\\\"\\s*:\\s*)"
+ "(?:\\\"(?:\\\\.|[^\\\"])*\\\"|'(?:\\\\.|[^'])*'|"
+ "\\[[^\\]]*\\]|[^,}\\r\\n]*(?=[,}\\r\\n]|$))");
private static final Pattern KEY_VALUE_SECRET = Pattern.compile(
"(?i)(\\b" + SECRET_FIELD + "\\b\\s*(?:=|:)\\s*)"
+ "(?:\\\"(?:\\\\.|[^\\\"])*\\\"|'(?:\\\\.|[^'])*'|"
+ "[^&,;}\\r\\n]*?(?=\\s+[a-z0-9_-]+\\s*(?:=|:)|[&,;}\\r\\n]|$))");
private static final Pattern AUTH_SCHEME = Pattern.compile(
"(?i)\\b(?:Bearer|Basic)\\s+[^\\s\\\"'<>]+");
private static final Pattern JWT = Pattern.compile(
"\\beyJ[A-Za-z0-9_-]{5,}\\.[A-Za-z0-9_-]{5,}(?:\\.[A-Za-z0-9_-]{5,})?\\b");
private static final Pattern OPENAI_STYLE_KEY = Pattern.compile(
"\\bsk[-_][A-Za-z0-9][A-Za-z0-9._~+/-]{7,}\\b");
private static final Pattern URI_PASSWORD = Pattern.compile(
"(?i)((?:jdbc:)?[a-z][a-z0-9+.-]*://[^\\s/@:]+:)([^\\s/@]+)(@)");
private SafeRequestLogUtils() {
}
/**
* Keeps only the path portion and removes query/fragment data entirely.
*/
public static String sanitizeUri(String uri) {
if (StringUtils.isBlank(uri)) {
return uri;
}
int suffixIndex = firstNonNegative(uri.indexOf('?'), uri.indexOf('#'));
String safe = suffixIndex >= 0 ? uri.substring(0, suffixIndex) : uri;
safe = ONLINE_TOKEN_PATH.matcher(safe).replaceAll("$1" + REDACTED);
safe = MYSELF_TOKEN_PATH.matcher(safe).replaceAll("$1" + REDACTED);
safe = GENERIC_SECRET_PATH.matcher(safe).replaceAll("$1" + REDACTED);
safe = sanitizeUnstructuredText(safe);
return abbreviate(safe, MAX_URI_LENGTH);
}
/**
* Sanitizes structured JSON recursively and falls back to bounded text patterns.
*/
public static String sanitizeText(String value) {
if (StringUtils.isBlank(value)) {
return value;
}
String trimmed = value.trim();
if (looksLikeJson(trimmed)) {
try {
Object parsed = OBJECT_MAPPER.readValue(trimmed, Object.class);
return abbreviate(writeSanitized(parsed), MAX_TEXT_LENGTH);
} catch (JsonProcessingException ignored) {
// Invalid JSON is still protected by the unstructured boundary below.
}
}
return abbreviate(sanitizeUnstructuredText(value), MAX_TEXT_LENGTH);
}
/**
* Recursively sanitizes maps, iterables, arrays, POJOs and JSON-compatible values.
*/
public static String sanitizeObject(Object value) {
if (value == null) {
return null;
}
return abbreviate(writeSanitized(value), MAX_TEXT_LENGTH);
}
private static String writeSanitized(Object value) {
Object safe = sanitizeValue(value, null, 0, new IdentityHashMap<>());
if (safe instanceof String text) {
return text;
}
try {
return OBJECT_MAPPER.writeValueAsString(safe);
} catch (JsonProcessingException ignored) {
return "[UNSERIALIZABLE:" + safeType(value) + "]";
}
}
private static Object sanitizeValue(Object value, String fieldName, int depth,
IdentityHashMap<Object, Boolean> seen) {
if (isSecretKey(fieldName)) {
return REDACTED;
}
if (value == null || value instanceof Number || value instanceof Boolean
|| value instanceof Character || value instanceof Enum<?>) {
return value;
}
if (value instanceof CharSequence text) {
return sanitizeUnstructuredText(text.toString());
}
if (value instanceof Throwable failure) {
return "[THROWABLE:" + failure.getClass().getName() + "]";
}
if (value instanceof Date || value instanceof TemporalAccessor) {
return value.toString();
}
if (depth >= MAX_DEPTH) {
return TRUNCATED;
}
if (seen.put(value, Boolean.TRUE) != null) {
return CYCLE;
}
try {
if (value instanceof Map<?, ?> map) {
Map<String, Object> safe = new LinkedHashMap<>();
int count = 0;
for (Map.Entry<?, ?> entry : map.entrySet()) {
if (count++ >= MAX_COLLECTION_ITEMS) {
safe.put(TRUNCATED, TRUNCATED);
break;
}
String key = safeMapKey(entry.getKey());
safe.put(key, sanitizeValue(entry.getValue(), key, depth + 1, seen));
}
return safe;
}
if (value instanceof Iterable<?> iterable) {
List<Object> safe = new ArrayList<>();
int count = 0;
for (Object item : iterable) {
if (count++ >= MAX_COLLECTION_ITEMS) {
safe.add(TRUNCATED);
break;
}
safe.add(sanitizeValue(item, null, depth + 1, seen));
}
return safe;
}
if (value.getClass().isArray()) {
List<Object> safe = new ArrayList<>();
int length = Math.min(Array.getLength(value), MAX_COLLECTION_ITEMS);
for (int index = 0; index < length; index++) {
safe.add(sanitizeValue(Array.get(value, index), null, depth + 1, seen));
}
if (Array.getLength(value) > length) {
safe.add(TRUNCATED);
}
return safe;
}
try {
Object converted = OBJECT_MAPPER.convertValue(value, Object.class);
if (converted == value || converted == null) {
return "[OBJECT:" + safeType(value) + "]";
}
return sanitizeValue(converted, fieldName, depth + 1, seen);
} catch (IllegalArgumentException | StackOverflowError ignored) {
return "[OBJECT:" + safeType(value) + "]";
}
} finally {
seen.remove(value);
}
}
private static String sanitizeUnstructuredText(String value) {
String safe = AUTH_SCHEME.matcher(value).replaceAll("[AUTH " + REDACTED + "]");
safe = JSON_SECRET.matcher(safe).replaceAll("$1\"" + REDACTED + "\"");
safe = KEY_VALUE_SECRET.matcher(safe).replaceAll("$1" + REDACTED);
safe = JWT.matcher(safe).replaceAll(REDACTED);
safe = OPENAI_STYLE_KEY.matcher(safe).replaceAll(REDACTED);
safe = URI_PASSWORD.matcher(safe).replaceAll("$1" + REDACTED + "$3");
return safe;
}
private static boolean isSecretKey(String fieldName) {
if (fieldName == null) {
return false;
}
String normalized = fieldName.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9]", "");
return SECRET_KEYS.contains(normalized)
|| normalized.contains("token")
|| normalized.contains("authorization")
|| normalized.contains("apikey")
|| normalized.contains("secret")
|| normalized.contains("signingkey")
|| normalized.contains("signature")
|| normalized.contains("password")
|| normalized.contains("passwd")
|| normalized.contains("credential");
}
private static String safeMapKey(Object key) {
if (key == null) {
return "null";
}
if (key instanceof CharSequence || key instanceof Number || key instanceof Enum<?>) {
return sanitizeUnstructuredText(String.valueOf(key));
}
return "[KEY:" + safeType(key) + "]";
}
private static boolean looksLikeJson(String value) {
return (value.startsWith("{") && value.endsWith("}"))
|| (value.startsWith("[") && value.endsWith("]"));
}
private static int firstNonNegative(int left, int right) {
if (left < 0) {
return right;
}
if (right < 0) {
return left;
}
return Math.min(left, right);
}
private static String safeType(Object value) {
return value == null ? "null" : value.getClass().getSimpleName();
}
private static String abbreviate(String value, int maximumLength) {
if (value == null || value.length() <= maximumLength) {
return value;
}
return value.substring(0, maximumLength) + "...(truncated)";
}
}