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

@@ -47,6 +47,12 @@
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,56 @@
package org.ruoyi.common.mybatis.config;
import com.p6spy.engine.spy.appender.MessageFormattingStrategy;
import java.util.regex.Pattern;
/**
* P6Spy formatter that deliberately ignores the bound SQL argument.
*
* <p>P6Spy supplies both the JDBC prepared statement and a reconstructed statement with
* parameter values. The latter can contain passwords, API keys and tokens, so this formatter
* records only a bounded, literal-free structural form of the prepared statement.</p>
*/
public final class SafeP6SpyMessageFormatter implements MessageFormattingStrategy {
private static final int MAX_SQL_LENGTH = 4096;
private static final Pattern SQL_STRING_LITERAL = Pattern.compile("'(?:''|\\\\.|[^'\\\\])*'");
private static final Pattern SQL_DOLLAR_QUOTED_LITERAL = Pattern.compile(
"\\$[A-Za-z0-9_]*\\$.*?\\$[A-Za-z0-9_]*\\$", Pattern.DOTALL);
private static final Pattern SECRET_SHAPED_TOKEN = Pattern.compile(
"(?i)\\b(?:sk[-_][A-Za-z0-9._~+/-]{8,}|AKIA[A-Z0-9]{12,})\\b");
private static final Pattern SQL_LINE_COMMENT = Pattern.compile("--[^\\r\\n]*");
private static final Pattern SQL_BLOCK_COMMENT = Pattern.compile("/\\*.*?\\*/", Pattern.DOTALL);
private static final Pattern WHITESPACE = Pattern.compile("\\s+");
@Override
public String formatMessage(int connectionId, String now, long elapsed, String category,
String prepared, String sql, String url) {
String statement = structuralPreparedSql(prepared);
return "%s | %d ms | %s | connection %d | %s".formatted(
safeLabel(now), elapsed, safeLabel(category), connectionId, statement);
}
static String structuralPreparedSql(String prepared) {
if (prepared == null || prepared.isBlank()) {
return "[prepared-sql-unavailable]";
}
String safe = SQL_BLOCK_COMMENT.matcher(prepared).replaceAll(" ");
safe = SQL_LINE_COMMENT.matcher(safe).replaceAll(" ");
safe = SQL_DOLLAR_QUOTED_LITERAL.matcher(safe).replaceAll("?");
safe = SQL_STRING_LITERAL.matcher(safe).replaceAll("'?'");
safe = SECRET_SHAPED_TOKEN.matcher(safe).replaceAll("[REDACTED]");
safe = WHITESPACE.matcher(safe).replaceAll(" ").trim();
if (safe.length() > MAX_SQL_LENGTH) {
return safe.substring(0, MAX_SQL_LENGTH) + "...(truncated)";
}
return safe;
}
private static String safeLabel(String value) {
if (value == null) {
return "";
}
return value.replaceAll("[\\r\\n|]", "_");
}
}

View File

@@ -11,6 +11,8 @@ import org.springframework.dao.DuplicateKeyException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.regex.Pattern;
/**
* Mybatis异常处理器
*
@@ -20,13 +22,17 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class MybatisExceptionHandler {
private static final Pattern CREDENTIAL_PATH_SEGMENT = Pattern.compile(
"(?i)(/(?:token(?:[-_]?id)?|access[-_]?token|refresh[-_]?token|api[-_]?key|"
+ "monitor/online(?:/myself)?|myself)/)([^/?#;\\s]+)");
/**
* 主键或UNIQUE索引,数据重复异常
*/
@ExceptionHandler(DuplicateKeyException.class)
public R<Void> handleDuplicateKeyException(DuplicateKeyException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',数据库中已存在记录'{}'", requestURI, e.getMessage());
log.error("database_request_failed category=DUPLICATE_KEY method={} path={} exceptionType={}",
request.getMethod(), safePath(request), e.getClass().getName());
return R.fail(HttpStatus.HTTP_CONFLICT, "数据库中已存在该记录,请联系管理员确认");
}
@@ -35,18 +41,20 @@ public class MybatisExceptionHandler {
*/
@ExceptionHandler(MyBatisSystemException.class)
public R<Void> handleCannotFindDataSourceException(MyBatisSystemException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
Throwable root = getRootCause(e);
if (root instanceof NotLoginException) {
log.error("请求地址'{}',认证失败'{}',无法访问系统资源", requestURI, root.getMessage());
log.error("database_request_failed category=AUTHENTICATION method={} path={} exceptionType={}",
request.getMethod(), safePath(request), root.getClass().getName());
return R.fail(HttpStatus.HTTP_UNAUTHORIZED, "认证失败,无法访问系统资源");
}
if (root instanceof CannotFindDataSourceException) {
log.error("请求地址'{}', 未找到数据源", requestURI);
log.error("database_request_failed category=DATA_SOURCE_NOT_FOUND method={} path={} exceptionType={}",
request.getMethod(), safePath(request), root.getClass().getName());
return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, "未找到数据源,请联系管理员确认");
}
log.error("请求地址'{}', Mybatis系统异常", requestURI, e);
return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, e.getMessage());
log.error("database_request_failed category=MYBATIS_SYSTEM method={} path={} exceptionType={} rootType={}",
request.getMethod(), safePath(request), e.getClass().getName(), root.getClass().getName());
return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, "数据库访问异常,请联系管理员确认");
}
/**
@@ -86,4 +94,16 @@ public class MybatisExceptionHandler {
return null;
}
private static String safePath(HttpServletRequest request) {
String path = request.getRequestURI();
if (path == null) {
return null;
}
int queryIndex = path.indexOf('?');
if (queryIndex >= 0) {
path = path.substring(0, queryIndex);
}
return CREDENTIAL_PATH_SEGMENT.matcher(path).replaceAll("$1[REDACTED]");
}
}

View File

@@ -1,11 +1,9 @@
# p6spy 性能分析插件配置文件
modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory
modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory
# 自定义日志打印
logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
#日志输出到控制台
appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
# 使用日志系统记录 sql
#appender=com.p6spy.engine.spy.appender.Slf4JLogger
logMessageFormat=org.ruoyi.common.mybatis.config.SafeP6SpyMessageFormatter
# 使用日志系统记录脱敏后的 prepared SQL 结构;formatter 永不消费 bound SQL 参数。
appender=com.p6spy.engine.spy.appender.Slf4JLogger
# 取消JDBC URL前缀
useprefix=true
# 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset.