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

@@ -27,6 +27,17 @@
<artifactId>ruoyi-common-json</artifactId>
</dependency>
<dependency>
<groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -1,6 +1,5 @@
package org.ruoyi.common.log.aspect;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
@@ -22,6 +21,7 @@ import org.ruoyi.common.log.annotation.Log;
import org.ruoyi.common.log.enums.BusinessStatus;
import org.ruoyi.common.log.event.OperLogEvent;
import org.ruoyi.common.satoken.utils.LoginHelper;
import org.ruoyi.common.web.utils.SafeRequestLogUtils;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.http.HttpMethod;
import org.springframework.validation.BindingResult;
@@ -42,7 +42,11 @@ public class LogAspect {
/**
* 排除敏感属性字段
*/
public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" };
public static final String[] EXCLUDE_PROPERTIES = {
"password", "oldPassword", "newPassword", "confirmPassword", "apiKey",
"clientSecret", "secret", "secretKey", "token", "tokenId", "accessToken",
"refreshToken", "authorization", "credential", "privateKey"
};
/**
@@ -91,14 +95,15 @@ public class LogAspect {
// 请求的地址
String ip = ServletUtils.getClientIP();
operLog.setOperIp(ip);
operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255));
operLog.setOperUrl(StringUtils.substring(SafeRequestLogUtils.sanitizeUri(
ServletUtils.getRequest().getRequestURI()), 0, 255));
LoginUser loginUser = LoginHelper.getLoginUser();
operLog.setOperName(loginUser.getUsername());
operLog.setDeptName(loginUser.getDeptName());
if (e != null) {
operLog.setStatus(BusinessStatus.FAIL.ordinal());
operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 3800));
operLog.setErrorMsg("Request failed: " + e.getClass().getSimpleName());
}
// 设置方法名称
String className = joinPoint.getTarget().getClass().getName();
@@ -116,7 +121,7 @@ public class LogAspect {
SpringUtils.context().publishEvent(operLog);
} catch (Exception exp) {
// 记录本地异常日志
log.error("异常信息:{}", exp.getMessage());
log.error("Operation audit failed: {}", exp.getClass().getSimpleName());
} finally {
KEY_CACHE.remove();
}
@@ -143,7 +148,9 @@ public class LogAspect {
}
// 是否需要保存response,参数和值
if (log.isSaveResponseData() && ObjectUtil.isNotNull(jsonResult)) {
operLog.setJsonResult(StringUtils.substring(JsonUtils.toJsonString(jsonResult), 0, 3800));
String responseJson = SensitiveLogSanitizer.sanitizeJson(
JsonUtils.toJsonString(jsonResult), log.excludeParamNames());
operLog.setJsonResult(StringUtils.substring(responseJson, 0, 3800));
}
}
@@ -160,9 +167,9 @@ public class LogAspect {
String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames);
operLog.setOperParam(StringUtils.substring(params, 0, 3800));
} else {
MapUtil.removeAny(paramsMap, EXCLUDE_PROPERTIES);
MapUtil.removeAny(paramsMap, excludeParamNames);
operLog.setOperParam(StringUtils.substring(JsonUtils.toJsonString(paramsMap), 0, 3800));
String params = SensitiveLogSanitizer.sanitizeJson(
JsonUtils.toJsonString(paramsMap), ArrayUtil.addAll(excludeParamNames, EXCLUDE_PROPERTIES));
operLog.setOperParam(StringUtils.substring(params, 0, 3800));
}
}
@@ -177,26 +184,7 @@ public class LogAspect {
String[] exclude = ArrayUtil.addAll(excludeParamNames, EXCLUDE_PROPERTIES);
for (Object o : paramsArray) {
if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
String str = "";
if (o instanceof List<?> list) {
List<Dict> list1 = new ArrayList<>();
for (Object obj : list) {
String str1 = JsonUtils.toJsonString(obj);
Dict dict = JsonUtils.parseMap(str1);
if (MapUtil.isNotEmpty(dict)) {
MapUtil.removeAny(dict, exclude);
list1.add(dict);
}
}
str = JsonUtils.toJsonString(list1);
} else {
str = JsonUtils.toJsonString(o);
Dict dict = JsonUtils.parseMap(str);
if (MapUtil.isNotEmpty(dict)) {
MapUtil.removeAny(dict, exclude);
str = JsonUtils.toJsonString(dict);
}
}
String str = SensitiveLogSanitizer.sanitizeJson(JsonUtils.toJsonString(o), exclude);
params.add(str);
}
}

View File

@@ -0,0 +1,95 @@
package org.ruoyi.common.log.aspect;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import org.ruoyi.common.web.utils.SafeRequestLogUtils;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/** Recursively removes credential fields from operation-audit JSON. */
final class SensitiveLogSanitizer {
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final Set<String> SENSITIVE_FIELDS = Set.of(
"password", "oldpassword", "newpassword", "confirmpassword", "passwd", "pwd",
"apikey", "clientsecret", "secret", "secretkey", "accesskey", "privatekey",
"token", "tokenid", "accesstoken", "refreshtoken", "authorization", "credential",
"credentials", "dbpassword"
);
private SensitiveLogSanitizer() {
}
static String sanitizeJson(String json, String... additionalSensitiveFields) {
if (json == null || json.isBlank()) {
return json;
}
try {
JsonNode root = MAPPER.readTree(json);
JsonNode safeRoot = redact(root, normalizedFields(additionalSensitiveFields));
return MAPPER.writeValueAsString(safeRoot);
} catch (Exception ignored) {
// Never fall back to the raw value: it may be exactly the malformed secret payload.
return SafeRequestLogUtils.sanitizeText(json);
}
}
private static Set<String> normalizedFields(String[] additionalSensitiveFields) {
Set<String> fields = new HashSet<>(SENSITIVE_FIELDS);
if (additionalSensitiveFields != null) {
Arrays.stream(additionalSensitiveFields)
.filter(value -> value != null && !value.isBlank())
.map(SensitiveLogSanitizer::normalize)
.forEach(fields::add);
}
return fields;
}
private static JsonNode redact(JsonNode node, Set<String> sensitiveFields) {
if (node == null) {
return null;
}
if (node instanceof ObjectNode object) {
Iterator<Map.Entry<String, JsonNode>> fields = object.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> field = fields.next();
if (sensitiveFields.contains(normalize(field.getKey()))) {
fields.remove();
} else {
JsonNode value = field.getValue();
if (value != null && value.isTextual()) {
object.set(field.getKey(), TextNode.valueOf(
SafeRequestLogUtils.sanitizeText(value.textValue())));
} else {
object.set(field.getKey(), redact(value, sensitiveFields));
}
}
}
} else if (node instanceof ArrayNode array) {
for (int index = 0; index < array.size(); index++) {
JsonNode value = array.get(index);
if (value != null && value.isTextual()) {
array.set(index, TextNode.valueOf(
SafeRequestLogUtils.sanitizeText(value.textValue())));
} else {
array.set(index, redact(value, sensitiveFields));
}
}
} else if (node.isTextual()) {
return TextNode.valueOf(SafeRequestLogUtils.sanitizeText(node.textValue()));
}
return node;
}
private static String normalize(String fieldName) {
return fieldName.replaceAll("[^A-Za-z0-9]", "").toLowerCase(Locale.ROOT);
}
}