mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-11-15 07:20:40 +00:00
feat: enhance coding harness, model security and system features
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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("系统异常,请联系管理员");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user