소스 검색

feat(security): 添加XSS和SQL注入防护过滤器

- 新增恶意HTTP请求异常类 BadHttpRequestException
- 实现XSS和SQL注入防护过滤器 XssAndSqlInjectionFilter
- 添加安全过滤器配置类 FilterConfig
- 新增安全工具类 SecurityUtils 提供XSS和SQL注入检测方法
- 添加安全配置属性类 SecurityFilterProperties
- 扩展字符串工具类 StringUtils
- 在全局异常处理器中添加恶意请求异常处理
- 配置异步线程池参数调整
- 禁用Jakarta Annotations API依赖
wzq 1 주 전
부모
커밋
0e61151284

+ 48 - 0
national-motion-base-core/src/main/java/org/jeecg/common/constant/SecurityFilterProperties.java

@@ -0,0 +1,48 @@
+package org.jeecg.common.constant;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 安全过滤器配置属性
+ * <p>
+ * 用于配置 XSS 和 SQL 注入防护过滤器的行为
+ *
+ * @author zsElectric
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "security.filter")
+public class SecurityFilterProperties {
+
+    /**
+     * 是否启用 XSS 防护
+     */
+    private Boolean xssEnabled = true;
+
+    /**
+     * 是否启用 SQL 注入防护
+     */
+    private Boolean sqlInjectionEnabled = true;
+
+    /**
+     * SQL 注入检测严格模式
+     * true: 严格模式,可能误判一些正常输入
+     * false: 宽松模式,减少误判但可能漏掉一些攻击
+     */
+    private Boolean sqlStrictMode = false;
+
+    /**
+     * 排除的 URL 路径(不进行安全检查)
+     */
+    private List<String> excludeUrls = new ArrayList<>();
+
+    /**
+     * 需要检查的请求头
+     */
+    private List<String> checkHeaders = new ArrayList<>();
+}

+ 22 - 0
national-motion-base-core/src/main/java/org/jeecg/common/exception/BadHttpRequestException.java

@@ -0,0 +1,22 @@
+package org.jeecg.common.exception;
+
+import lombok.Getter;
+
+/**
+ * 恶意HTTP请求异常
+ * <p>
+ * 用于标识XSS攻击、SQL注入等恶意请求
+ * 
+ * @author zsElectric
+ */
+@Getter
+public class BadHttpRequestException extends RuntimeException {
+
+    public BadHttpRequestException(String message) {
+        super(message);
+    }
+
+    public BadHttpRequestException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}

+ 11 - 0
national-motion-base-core/src/main/java/org/jeecg/common/exception/JeecgBootExceptionHandler.java

@@ -204,6 +204,17 @@ public class JeecgBootExceptionHandler {
         return Result.error("Redis 连接异常!");
     }
 
+    /**
+     * 处理恶意HTTP请求异常(XSS攻击、SQL注入等)
+     * 当检测到恶意请求时,会抛出 BadHttpRequestException 异常。
+     */
+    @ExceptionHandler(BadHttpRequestException.class)
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public <T> Result<T> handleBadHttpRequestException(BadHttpRequestException e) {
+        log.error("检测到恶意请求,异常原因:{}", e.getMessage(), e);
+        return Result.error("用户输入包含非法内容,请输入合法内容!");
+    }
+
 
 	/**
 	 * SQL注入风险,全局异常处理

+ 380 - 0
national-motion-base-core/src/main/java/org/jeecg/common/util/SecurityUtils.java

@@ -0,0 +1,380 @@
+package org.jeecg.common.util;
+
+import cn.hutool.core.util.StrUtil;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * 安全防护工具类
+ * <p>
+ * 提供 XSS 攻击和 SQL 注入防护的核心检测方法
+ *
+ * @author zsElectric
+ */
+@Slf4j
+public class SecurityUtils {
+
+    /**
+     * SQL 注入检测是否使用严格模式
+     * 默认为宽松模式以减少误判
+     */
+    private static volatile boolean sqlStrictMode = false;
+
+    /**
+     * 设置 SQL 注入检测模式
+     * 
+     * @param strictMode true 为严格模式,false 为宽松模式
+     */
+    public static void setSqlStrictMode(boolean strictMode) {
+        sqlStrictMode = strictMode;
+    }
+
+    /**
+     * XSS 攻击检测正则表达式
+     */
+    private static final Pattern[] XSS_PATTERNS = {
+            // Script 标签
+            Pattern.compile("<script[^>]*?>.*?</script>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL),
+            Pattern.compile("<script[^>]*?>", Pattern.CASE_INSENSITIVE),
+            Pattern.compile("</script>", Pattern.CASE_INSENSITIVE),
+            // JavaScript 事件
+            Pattern.compile("javascript:", Pattern.CASE_INSENSITIVE),
+            Pattern.compile("onerror\\s*=", Pattern.CASE_INSENSITIVE),
+            Pattern.compile("onload\\s*=", Pattern.CASE_INSENSITIVE),
+            Pattern.compile("onclick\\s*=", Pattern.CASE_INSENSITIVE),
+            Pattern.compile("onmouseover\\s*=", Pattern.CASE_INSENSITIVE),
+            Pattern.compile("onfocus\\s*=", Pattern.CASE_INSENSITIVE),
+            Pattern.compile("onblur\\s*=", Pattern.CASE_INSENSITIVE),
+            Pattern.compile("onsubmit\\s*=", Pattern.CASE_INSENSITIVE),
+            Pattern.compile("onreset\\s*=", Pattern.CASE_INSENSITIVE),
+            Pattern.compile("onselect\\s*=", Pattern.CASE_INSENSITIVE),
+            Pattern.compile("onchange\\s*=", Pattern.CASE_INSENSITIVE),
+            // iframe 标签
+            Pattern.compile("<iframe[^>]*?>.*?</iframe>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL),
+            Pattern.compile("<iframe[^>]*?>", Pattern.CASE_INSENSITIVE),
+            // embed、object 标签
+            Pattern.compile("<embed[^>]*?>", Pattern.CASE_INSENSITIVE),
+            Pattern.compile("<object[^>]*?>", Pattern.CASE_INSENSITIVE),
+            // eval、expression
+            Pattern.compile("eval\\s*\\(", Pattern.CASE_INSENSITIVE),
+            Pattern.compile("expression\\s*\\(", Pattern.CASE_INSENSITIVE),
+            // vbscript
+            Pattern.compile("vbscript:", Pattern.CASE_INSENSITIVE),
+            // img 标签的 src
+            Pattern.compile("<img[^>]+src[\\s]*=[\\s]*['\"]?javascript:", Pattern.CASE_INSENSITIVE),
+            // style 中的 expression
+            Pattern.compile("style\\s*=.*expression", Pattern.CASE_INSENSITIVE),
+            // base64 编码的脚本
+            Pattern.compile("data:text/html;base64", Pattern.CASE_INSENSITIVE),
+            // SVG
+            Pattern.compile("<svg[^>]*?>.*?</svg>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL),
+            // Meta 标签
+            Pattern.compile("<meta[^>]*?>", Pattern.CASE_INSENSITIVE),
+            // Link 标签
+            Pattern.compile("<link[^>]*?>", Pattern.CASE_INSENSITIVE)
+    };
+
+    /**
+     * SQL 注入危险关键词
+     */
+    private static final Set<String> SQL_KEYWORDS = new HashSet<>(Arrays.asList(
+            // DML 语句
+            "select", "insert", "update", "delete",
+            // DDL 语句
+            "drop", "create", "alter", "truncate",
+            // DCL 语句
+            "grant", "revoke",
+            // 联合查询
+            "union", "join",
+            // 系统函数和存储过程
+            "exec", "execute", "xp_cmdshell", "sp_executesql",
+            // 信息获取
+            "information_schema", "mysql.user", "sys.",
+            // 条件判断
+            "case", "when", "then", "else", "end",
+            // 其他危险操作
+            "declare", "cast", "convert", "char", "chr",
+            "concat", "load_file", "into outfile", "into dumpfile",
+            "benchmark", "sleep", "waitfor", "delay",
+            // 子查询
+            "exists", "any", "all", "some"
+    ));
+
+    /**
+     * SQL 注入检测正则表达式
+     */
+    private static final Pattern[] SQL_INJECTION_PATTERNS = {
+            // SQL 注释
+            Pattern.compile("('.+--)|(--)|(;)|(\\|{2})"),
+            // SQL 函数调用
+            Pattern.compile("\\bexec(ute)?\\s*\\(", Pattern.CASE_INSENSITIVE),
+            // union 查询
+            Pattern.compile("\\bunion\\b.*\\bselect\\b", Pattern.CASE_INSENSITIVE),
+            // 多语句
+            Pattern.compile(";.*?(select|insert|update|delete|drop|create|alter)", Pattern.CASE_INSENSITIVE),
+            // 16 进制编码
+            Pattern.compile("0x[0-9a-f]+", Pattern.CASE_INSENSITIVE),
+            // 字符串拼接
+            Pattern.compile("\\bconcat\\s*\\(", Pattern.CASE_INSENSITIVE),
+            // sleep 函数
+            Pattern.compile("\\bsleep\\s*\\(", Pattern.CASE_INSENSITIVE),
+            // benchmark 函数
+            Pattern.compile("\\bbenckmark\\s*\\(", Pattern.CASE_INSENSITIVE),
+            // waitfor delay
+            Pattern.compile("\\bwaitfor\\s+\\bdelay\\b", Pattern.CASE_INSENSITIVE),
+            // 子查询
+            Pattern.compile("\\bsubstr\\s*\\(", Pattern.CASE_INSENSITIVE),
+            Pattern.compile("\\bsubstring\\s*\\(", Pattern.CASE_INSENSITIVE)
+    };
+
+    /**
+     * 检测 XSS 攻击
+     *
+     * @param value 待检测的字符串
+     * @return 如果检测到 XSS 攻击返回 true,否则返回 false
+     */
+    public static boolean containsXss(String value) {
+        if (StrUtil.isBlank(value)) {
+            return false;
+        }
+
+        // 解码 URL 编码
+        String decodedValue = urlDecode(value);
+
+        // 使用正则表达式检测
+        for (Pattern pattern : XSS_PATTERNS) {
+            if (pattern.matcher(decodedValue).find()) {
+                log.warn("检测到 XSS 攻击,匹配模式: {}, 内容: {}", pattern.pattern(), value);
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * 检测 SQL 注入
+     *
+     * @param value 待检测的字符串
+     * @return 如果检测到 SQL 注入返回 true,否则返回 false
+     */
+    public static boolean containsSqlInjection(String value) {
+        if (StrUtil.isBlank(value)) {
+            return false;
+        }
+
+        String lowerValue = value.toLowerCase();
+
+        // 检查注释符号(更严格的检测)
+        if (lowerValue.contains("--") || lowerValue.contains("/*") || lowerValue.contains("*/") || lowerValue.contains("#")) {
+            // 检查是否是真正的注释而不是普通文本
+            if (lowerValue.matches(".*\\s(--|#).*") || lowerValue.contains("/*") || lowerValue.contains("*/")) {
+                log.warn("检测到 SQL 注入注释符号: {}, 内容: {}", "--/#/*", value);
+                return true;
+            }
+        }
+
+        // 检查危险关键词(使用更精确的匹配规则)
+        for (String keyword : SQL_KEYWORDS) {
+            // 使用单词边界进行匹配,避免误判(例如:"selection" 不应匹配 "select")
+            // 同时确保关键词前后不是字母数字字符
+            String pattern = "([^a-zA-Z0-9]|^)" + keyword + "([^a-zA-Z0-9]|$)";
+            if (Pattern.compile(pattern, Pattern.CASE_INSENSITIVE).matcher(lowerValue).find()) {
+                // 进一步检查是否为真实攻击而非正常文本
+                // 例如:"select" 在 "selected" 中是正常文本,但在 "select * from" 中可能是攻击
+                if (isRealSqlInjection(keyword, lowerValue)) {
+                    log.warn("检测到 SQL 注入关键词: {}, 内容: {}", keyword, value);
+                    return true;
+                }
+            }
+        }
+
+        // 使用正则表达式检测
+        for (Pattern pattern : SQL_INJECTION_PATTERNS) {
+            if (pattern.matcher(value).find()) {
+                log.warn("检测到 SQL 注入,匹配模式: {}, 内容: {}", pattern.pattern(), value);
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * 清理 XSS 攻击内容(转义特殊字符)
+     *
+     * @param value 待清理的字符串
+     * @return 清理后的字符串
+     */
+    public static String cleanXss(String value) {
+        if (StrUtil.isBlank(value)) {
+            return value;
+        }
+
+        // HTML 实体编码
+        value = value.replace("&", "&amp;")
+                .replace("<", "&lt;")
+                .replace(">", "&gt;")
+                .replace("\"", "&quot;")
+                .replace("'", "&#x27;")
+                .replace("/", "&#x2F;");
+
+        return value;
+    }
+
+    /**
+     * URL 解码(支持多次编码)
+     *
+     * @param value 待解码的字符串
+     * @return 解码后的字符串
+     */
+    private static String urlDecode(String value) {
+        String decoded = value;
+        try {
+            // 最多解码 3 次,防止多重编码绕过
+            for (int i = 0; i < 3; i++) {
+                String temp = java.net.URLDecoder.decode(decoded, "UTF-8");
+                if (temp.equals(decoded)) {
+                    break;
+                }
+                decoded = temp;
+            }
+        } catch (Exception e) {
+            log.warn("URL 解码失败: {}", value, e);
+        }
+        return decoded;
+    }
+
+    /**
+     * 验证输入是否安全(综合检查 XSS 和 SQL 注入)
+     *
+     * @param value 待验证的字符串
+     * @return 如果输入安全返回 true,否则返回 false
+     */
+    public static boolean isSafeInput(String value) {
+        return !containsXss(value) && !containsSqlInjection(value);
+    }
+
+    /**
+     * 判断是否为真实的SQL注入攻击
+     * 
+     * @param keyword 检测到的关键词
+     * @param value   待检测的字符串(小写)
+     * @return 如果是真实攻击返回 true,否则返回 false
+     */
+    private static boolean isRealSqlInjection(String keyword, String value) {
+        if (!sqlStrictMode) {
+            // 在宽松模式下,只对明显的攻击模式进行拦截
+            switch (keyword) {
+                case "select":
+                    // 只有当 select 后面跟着典型的 SQL 结构时才认为是攻击
+                    boolean isSelectAttack = value.matches(".*select\\s+[a-z0-9_*]+\\s+from\\s+[a-z0-9_]+.*") || 
+                           value.matches(".*select\\s+\\*\\s+from\\s+[a-z0-9_]+.*");
+                    if (isSelectAttack) {
+                        log.debug("检测到可能的 SELECT 攻击: {}", value);
+                    }
+                    return isSelectAttack;
+                case "insert":
+                    boolean isInsertAttack = value.contains("insert into");
+                    if (isInsertAttack) {
+                        log.debug("检测到可能的 INSERT 攻击: {}", value);
+                    }
+                    return isInsertAttack;
+                case "update":
+                    boolean isUpdateAttack = value.contains("update ") && value.contains(" set ");
+                    if (isUpdateAttack) {
+                        log.debug("检测到可能的 UPDATE 攻击: {}", value);
+                    }
+                    return isUpdateAttack;
+                case "delete":
+                    boolean isDeleteAttack = value.contains("delete from");
+                    if (isDeleteAttack) {
+                        log.debug("检测到可能的 DELETE 攻击: {}", value);
+                    }
+                    return isDeleteAttack;
+                case "drop":
+                case "create":
+                case "alter":
+                case "truncate":
+                    boolean isDdlAttack = value.contains(keyword + " ");
+                    if (isDdlAttack) {
+                        log.debug("检测到可能的 DDL 攻击 ({}): {}", keyword, value);
+                    }
+                    return isDdlAttack;
+                case "union":
+                    boolean isUnionAttack = value.contains("union select");
+                    if (isUnionAttack) {
+                        log.debug("检测到可能的 UNION 攻击: {}", value);
+                    }
+                    return isUnionAttack;
+                case "exec":
+                case "execute":
+                    boolean isExecAttack = value.contains(keyword + "(");
+                    if (isExecAttack) {
+                        log.debug("检测到可能的 EXEC 攻击 ({}): {}", keyword, value);
+                    }
+                    return isExecAttack;
+                default:
+                    // 对于其他关键词,采用宽松策略,避免误判正常用户名等
+                    return false;
+            }
+        } else {
+            // 严格模式下保持原来的逻辑
+            switch (keyword) {
+                case "select":
+                    // select 通常是攻击的一部分,后面跟着列名和 from
+                    boolean isSelectAttack = value.matches(".*select\\s+[a-z0-9_*]+\\s+from\\s+[a-z0-9_]+.*") || 
+                           value.matches(".*select\\s+\\*\\s+from\\s+[a-z0-9_]+.*");
+                    if (isSelectAttack) {
+                        log.debug("[严格模式] 检测到可能的 SELECT 攻击: {}", value);
+                    }
+                    return isSelectAttack;
+                case "insert":
+                    // insert 通常是攻击的一部分,后面跟着 into
+                    boolean isInsertAttack = value.contains("insert into");
+                    if (isInsertAttack) {
+                        log.debug("[严格模式] 检测到可能的 INSERT 攻击: {}", value);
+                    }
+                    return isInsertAttack;
+                case "update":
+                    // update 通常是攻击的一部分,后面跟着 set
+                    boolean isUpdateAttack = value.contains("update ") && value.contains(" set ");
+                    if (isUpdateAttack) {
+                        log.debug("[严格模式] 检测到可能的 UPDATE 攻击: {}", value);
+                    }
+                    return isUpdateAttack;
+                case "delete":
+                    // delete 通常是攻击的一部分,后面跟着 from
+                    boolean isDeleteAttack = value.contains("delete from");
+                    if (isDeleteAttack) {
+                        log.debug("[严格模式] 检测到可能的 DELETE 攻击: {}", value);
+                    }
+                    return isDeleteAttack;
+                case "drop":
+                case "create":
+                case "alter":
+                case "truncate":
+                    // DDL 语句通常是攻击的一部分
+                    boolean isDdlAttack = value.contains(keyword + " ");
+                    if (isDdlAttack) {
+                        log.debug("[严格模式] 检测到可能的 DDL 攻击 ({}): {}", keyword, value);
+                    }
+                    return isDdlAttack;
+                case "union":
+                    // union 通常是攻击的一部分,后面跟着 select
+                    boolean isUnionAttack = value.contains("union select");
+                    if (isUnionAttack) {
+                        log.debug("[严格模式] 检测到可能的 UNION 攻击: {}", value);
+                    }
+                    return isUnionAttack;
+                default:
+                    // 对于其他关键词,采用宽松策略,避免误判正常用户名等
+                    return false;
+            }
+        }
+    }
+}

+ 384 - 0
national-motion-base-core/src/main/java/org/jeecg/common/util/StringUtils.java

@@ -0,0 +1,384 @@
+package org.jeecg.common.util;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.lang.Validator;
+import cn.hutool.core.util.StrUtil;
+import org.springframework.util.AntPathMatcher;
+
+import java.nio.charset.Charset;
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 字符串工具类
+ *
+ * @author Lion Li
+ */
+public class StringUtils extends org.apache.commons.lang3.StringUtils {
+
+    public static final String SEPARATOR = ",";
+
+    public static final String SLASH = "/";
+
+    @Deprecated
+    private StringUtils() {
+    }
+
+    /**
+     * 获取参数不为空值
+     *
+     * @param str defaultValue 要判断的value
+     * @return value 返回值
+     */
+    public static String blankToDefault(String str, String defaultValue) {
+        return StrUtil.blankToDefault(str, defaultValue);
+    }
+
+    /**
+     * * 判断一个字符串是否为空串
+     *
+     * @param str String
+     * @return true:为空 false:非空
+     */
+    public static boolean isEmpty(String str) {
+        return StrUtil.isEmpty(str);
+    }
+
+    /**
+     * * 判断一个字符串是否为非空串
+     *
+     * @param str String
+     * @return true:非空串 false:空串
+     */
+    public static boolean isNotEmpty(String str) {
+        return !isEmpty(str);
+    }
+
+    /**
+     * 去空格
+     */
+    public static String trim(String str) {
+        return StrUtil.trim(str);
+    }
+
+    /**
+     * 截取字符串
+     *
+     * @param str   字符串
+     * @param start 开始
+     * @return 结果
+     */
+    public static String substring(final String str, int start) {
+        return substring(str, start, str.length());
+    }
+
+    /**
+     * 截取字符串
+     *
+     * @param str   字符串
+     * @param start 开始
+     * @param end   结束
+     * @return 结果
+     */
+    public static String substring(final String str, int start, int end) {
+        return StrUtil.sub(str, start, end);
+    }
+
+    /**
+     * 格式化文本, {} 表示占位符<br>
+     * 此方法只是简单将占位符 {} 按照顺序替换为参数<br>
+     * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可<br>
+     * 例:<br>
+     * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b<br>
+     * 转义{}: format("this is \\{} for {}", "a", "b") -> this is {} for a<br>
+     * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b<br>
+     *
+     * @param template 文本模板,被替换的部分用 {} 表示
+     * @param params   参数值
+     * @return 格式化后的文本
+     */
+    public static String format(String template, Object... params) {
+        return StrUtil.format(template, params);
+    }
+
+    /**
+     * 是否为http(s)://开头
+     *
+     * @param link 链接
+     * @return 结果
+     */
+    public static boolean ishttp(String link) {
+        return Validator.isUrl(link);
+    }
+
+    /**
+     * 字符串转set
+     *
+     * @param str 字符串
+     * @param sep 分隔符
+     * @return set集合
+     */
+    public static Set<String> str2Set(String str, String sep) {
+        return new HashSet<>(str2List(str, sep, true, false));
+    }
+
+    /**
+     * 字符串转list
+     *
+     * @param str         字符串
+     * @param sep         分隔符
+     * @param filterBlank 过滤纯空白
+     * @param trim        去掉首尾空白
+     * @return list集合
+     */
+    public static List<String> str2List(String str, String sep, boolean filterBlank, boolean trim) {
+        List<String> list = new ArrayList<>();
+        if (isEmpty(str)) {
+            return list;
+        }
+
+        // 过滤空白字符串
+        if (filterBlank && isBlank(str)) {
+            return list;
+        }
+        String[] split = str.split(sep);
+        for (String string : split) {
+            if (filterBlank && isBlank(string)) {
+                continue;
+            }
+            if (trim) {
+                string = trim(string);
+            }
+            list.add(string);
+        }
+
+        return list;
+    }
+
+    /**
+     * 查找指定字符串是否包含指定字符串列表中的任意一个字符串同时串忽略大小写
+     *
+     * @param cs                  指定字符串
+     * @param searchCharSequences 需要检查的字符串数组
+     * @return 是否包含任意一个字符串
+     */
+    public static boolean containsAnyIgnoreCase(CharSequence cs, CharSequence... searchCharSequences) {
+        return StrUtil.containsAnyIgnoreCase(cs, searchCharSequences);
+    }
+
+    /**
+     * 驼峰转下划线命名
+     */
+    public static String toUnderScoreCase(String str) {
+        return StrUtil.toUnderlineCase(str);
+    }
+
+    /**
+     * 是否包含字符串
+     *
+     * @param str  验证字符串
+     * @param strs 字符串组
+     * @return 包含返回true
+     */
+    public static boolean inStringIgnoreCase(String str, String... strs) {
+        return StrUtil.equalsAnyIgnoreCase(str, strs);
+    }
+
+    /**
+     * 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。 例如:HELLO_WORLD->HelloWorld
+     *
+     * @param name 转换前的下划线大写方式命名的字符串
+     * @return 转换后的驼峰式命名的字符串
+     */
+    public static String convertToCamelCase(String name) {
+        return StrUtil.upperFirst(StrUtil.toCamelCase(name));
+    }
+
+    /**
+     * 驼峰式命名法 例如:user_name->userName
+     */
+    public static String toCamelCase(String s) {
+        return StrUtil.toCamelCase(s);
+    }
+
+    /**
+     * 查找指定字符串是否匹配指定字符串列表中的任意一个字符串
+     *
+     * @param str  指定字符串
+     * @param strs 需要检查的字符串数组
+     * @return 是否匹配
+     */
+    public static boolean matches(String str, List<String> strs) {
+        if (isEmpty(str) || CollUtil.isEmpty(strs)) {
+            return false;
+        }
+        for (String pattern : strs) {
+            if (isMatch(pattern, str)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 判断url是否与规则配置:
+     * ? 表示单个字符;
+     * * 表示一层路径内的任意字符串,不可跨层级;
+     * ** 表示任意层路径;
+     *
+     * @param pattern 匹配规则
+     * @param url     需要匹配的url
+     */
+    public static boolean isMatch(String pattern, String url) {
+        AntPathMatcher matcher = new AntPathMatcher();
+        return matcher.match(pattern, url);
+    }
+
+    /**
+     * 数字左边补齐0,使之达到指定长度。注意,如果数字转换为字符串后,长度大于size,则只保留 最后size个字符。
+     *
+     * @param num  数字对象
+     * @param size 字符串指定长度
+     * @return 返回数字的字符串格式,该字符串为指定长度。
+     */
+    public static String padl(final Number num, final int size) {
+        return padl(num.toString(), size, '0');
+    }
+
+    /**
+     * 字符串左补齐。如果原始字符串s长度大于size,则只保留最后size个字符。
+     *
+     * @param s    原始字符串
+     * @param size 字符串指定长度
+     * @param c    用于补齐的字符
+     * @return 返回指定长度的字符串,由原字符串左补齐或截取得到。
+     */
+    public static String padl(final String s, final int size, final char c) {
+        final StringBuilder sb = new StringBuilder(size);
+        if (s != null) {
+            final int len = s.length();
+            if (s.length() <= size) {
+                sb.append(Convert.toStr(c).repeat(size - len));
+                sb.append(s);
+            } else {
+                return s.substring(len - size, len);
+            }
+        } else {
+            sb.append(Convert.toStr(c).repeat(Math.max(0, size)));
+        }
+        return sb.toString();
+    }
+
+    /**
+     * 切分字符串(分隔符默认逗号)
+     *
+     * @param str 被切分的字符串
+     * @return 分割后的数据列表
+     */
+    public static List<String> splitList(String str) {
+        return splitTo(str, Convert::toStr);
+    }
+
+    /**
+     * 切分字符串
+     *
+     * @param str       被切分的字符串
+     * @param separator 分隔符
+     * @return 分割后的数据列表
+     */
+    public static List<String> splitList(String str, String separator) {
+        return splitTo(str, separator, Convert::toStr);
+    }
+
+    /**
+     * 切分字符串自定义转换(分隔符默认逗号)
+     *
+     * @param str    被切分的字符串
+     * @param mapper 自定义转换
+     * @return 分割后的数据列表
+     */
+    public static <T> List<T> splitTo(String str, Function<? super Object, T> mapper) {
+        return splitTo(str, SEPARATOR, mapper);
+    }
+
+    /**
+     * 切分字符串自定义转换
+     *
+     * @param str       被切分的字符串
+     * @param separator 分隔符
+     * @param mapper    自定义转换
+     * @return 分割后的数据列表
+     */
+    public static <T> List<T> splitTo(String str, String separator, Function<? super Object, T> mapper) {
+        if (isBlank(str)) {
+            return new ArrayList<>(0);
+        }
+        return StrUtil.split(str, separator)
+            .stream()
+            .filter(Objects::nonNull)
+            .map(mapper)
+            .filter(Objects::nonNull)
+            .collect(Collectors.toList());
+    }
+
+    /**
+     * 不区分大小写检查 CharSequence 是否以指定的前缀开头。
+     *
+     * @param str     要检查的 CharSequence 可能为 null
+     * @param prefixs 要查找的前缀可能为 null
+     * @return 是否包含
+     */
+    public static boolean startWithAnyIgnoreCase(CharSequence str, CharSequence... prefixs) {
+        // 判断是否是以指定字符串开头
+        for (CharSequence prefix : prefixs) {
+            if (StringUtils.startsWithIgnoreCase(str, prefix)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 将字符串从源字符集转换为目标字符集
+     *
+     * @param input       原始字符串
+     * @param fromCharset 源字符集
+     * @param toCharset   目标字符集
+     * @return 转换后的字符串
+     */
+    public static String convert(String input, Charset fromCharset, Charset toCharset) {
+        if (isBlank(input)) {
+            return input;
+        }
+        try {
+            // 从源字符集获取字节
+            byte[] bytes = input.getBytes(fromCharset);
+            // 使用目标字符集解码
+            return new String(bytes, toCharset);
+        } catch (Exception e) {
+            return input;
+        }
+    }
+    /**
+     * 将可迭代对象中的元素使用逗号拼接成字符串
+     *
+     * @param iterable 可迭代对象,如 List、Set 等
+     * @return 拼接后的字符串
+     */
+    public static String joinComma(Iterable<?> iterable) {
+        return StringUtils.join(iterable, SEPARATOR);
+    }
+
+    /**
+     * 将数组中的元素使用逗号拼接成字符串
+     *
+     * @param array 任意类型的数组
+     * @return 拼接后的字符串
+     */
+    public static String joinComma(Object[] array) {
+        return StringUtils.join(array, SEPARATOR);
+    }
+
+}

+ 43 - 0
national-motion-base-core/src/main/java/org/jeecg/config/FilterConfig.java

@@ -0,0 +1,43 @@
+package org.jeecg.config;
+
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.jeecg.common.constant.SecurityFilterProperties;
+import org.jeecg.config.filter.XssAndSqlInjectionFilter;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 过滤器配置类
+ * <p>
+ * 配置 XSS 和 SQL 注入防护过滤器
+ *
+ * @author zsElectric
+ */
+@Slf4j
+@Configuration
+@RequiredArgsConstructor
+public class FilterConfig {
+    
+    private final SecurityFilterProperties securityFilterProperties;
+
+    /**
+     * 注册 XSS 和 SQL 注入防护过滤器
+     */
+    @Bean
+    public FilterRegistrationBean<XssAndSqlInjectionFilter> xssAndSqlInjectionFilter() {
+        log.info("注册 XSS 和 SQL 注入防护过滤器");
+        
+        FilterRegistrationBean<XssAndSqlInjectionFilter> registration = new FilterRegistrationBean<>();
+        registration.setFilter(new XssAndSqlInjectionFilter(securityFilterProperties));
+        registration.addUrlPatterns("/*");
+        registration.setName("xssAndSqlInjectionFilter");
+        // 设置过滤器优先级(数字越小优先级越高)
+        // 应该在 Spring Security 过滤器之前执行
+        registration.setOrder(1);
+        
+        return registration;
+    }
+}

+ 32 - 0
national-motion-base-core/src/main/java/org/jeecg/config/SecurityUtilsConfig.java

@@ -0,0 +1,32 @@
+package org.jeecg.config;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.jeecg.common.constant.SecurityFilterProperties;
+import org.jeecg.common.util.SecurityUtils;
+import org.springframework.context.annotation.Configuration;
+
+import javax.annotation.PostConstruct;
+
+/**
+ * 安全工具类配置
+ * <p>
+ * 用于初始化安全工具类的相关配置
+ *
+ * @author zsElectric
+ */
+@Slf4j
+@Configuration
+@RequiredArgsConstructor
+public class SecurityUtilsConfig {
+
+    private final SecurityFilterProperties securityFilterProperties;
+
+    @PostConstruct
+    public void init() {
+        // 设置 SQL 注入检测模式
+        SecurityUtils.setSqlStrictMode(securityFilterProperties.getSqlStrictMode());
+        log.info("安全工具类初始化完成,SQL 注入检测模式: {}", 
+                securityFilterProperties.getSqlStrictMode() ? "严格模式" : "宽松模式");
+    }
+}

+ 264 - 0
national-motion-base-core/src/main/java/org/jeecg/config/filter/XssAndSqlInjectionFilter.java

@@ -0,0 +1,264 @@
+package org.jeecg.config.filter;
+
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.StrUtil;
+
+import lombok.extern.slf4j.Slf4j;
+import org.jeecg.common.constant.SecurityFilterProperties;
+import org.jeecg.common.exception.BadHttpRequestException;
+import org.jeecg.common.util.SecurityUtils;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+
+import javax.servlet.*;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * XSS 和 SQL 注入防护过滤器
+ * <p>
+ * 对所有 HTTP 请求进行安全检查,包括:
+ * <ul>
+ *     <li>请求参数(Query String 和 Form Data)</li>
+ *     <li>请求体(JSON、XML 等)</li>
+ *     <li>请求头</li>
+ * </ul>
+ *
+ * @author zsElectric
+ */
+@Slf4j
+public class XssAndSqlInjectionFilter implements Filter {
+
+    private final SecurityFilterProperties properties;
+
+    /**
+     * 默认排除的 URL 路径(不进行安全检查)
+     */
+    private static final Set<String> DEFAULT_EXCLUDE_URLS = new HashSet<>(Arrays.asList(
+            "/api/v1/auth/captcha",
+            "/doc.html",
+            "/swagger-ui",
+            "/v3/api-docs",
+            "/webjars"
+    ));
+
+    /**
+     * 默认需要检查的请求头
+     */
+    private static final Set<String> DEFAULT_CHECK_HEADERS = new HashSet<>(Arrays.asList(
+            "User-Agent",
+            "Referer",
+            "X-Forwarded-For"
+    ));
+
+    public XssAndSqlInjectionFilter(SecurityFilterProperties properties) {
+        this.properties = properties;
+    }
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+            throws IOException, ServletException {
+
+        HttpServletRequest httpRequest = (HttpServletRequest) request;
+
+        // 检查是否为排除的 URL
+        if (isExcludedUrl(httpRequest)) {
+            chain.doFilter(request, response);
+            return;
+        }
+
+        try {
+            // 包装请求,进行安全检查
+            SecurityRequestWrapper wrappedRequest = new SecurityRequestWrapper(httpRequest);
+            chain.doFilter(wrappedRequest, response);
+        } catch (BadHttpRequestException e) {
+            log.error("检测到恶意请求,URL: {}, 错误信息: {}", httpRequest.getRequestURI(), e.getMessage());
+            throw e;
+        }
+    }
+
+    /**
+     * 检查 URL 是否在排除列表中
+     */
+    private boolean isExcludedUrl(HttpServletRequest request) {
+        String uri = request.getRequestURI();
+        
+        // 检查默认排除列表
+        if (DEFAULT_EXCLUDE_URLS.stream().anyMatch(uri::startsWith)) {
+            return true;
+        }
+        
+        // 检查配置的排除列表
+        if (properties.getExcludeUrls() != null) {
+            return properties.getExcludeUrls().stream().anyMatch(uri::startsWith);
+        }
+        
+        return false;
+    }
+
+    /**
+     * 安全请求包装类
+     */
+    private class SecurityRequestWrapper extends HttpServletRequestWrapper {
+
+        private byte[] body;
+
+        public SecurityRequestWrapper(HttpServletRequest request) throws IOException {
+            super(request);
+            // 缓存请求体
+            if (isJsonOrXmlRequest(request)) {
+                body = IoUtil.readBytes(request.getInputStream());
+                // 检查请求体
+                String bodyContent = new String(body, StandardCharsets.UTF_8);
+                if (StrUtil.isNotBlank(bodyContent)) {
+                    checkContent(bodyContent, "请求体");
+                }
+            }
+            // 检查请求头
+            checkHeaders();
+        }
+
+        @Override
+        public String getParameter(String name) {
+            String value = super.getParameter(name);
+            if (value != null) {
+                checkContent(value, "请求参数[" + name + "]");
+            }
+            return value;
+        }
+
+        @Override
+        public String[] getParameterValues(String name) {
+            String[] values = super.getParameterValues(name);
+            if (values != null) {
+                for (int i = 0; i < values.length; i++) {
+                    if (values[i] != null) {
+                        checkContent(values[i], "请求参数[" + name + "][" + i + "]");
+                    }
+                }
+            }
+            return values;
+        }
+
+        @Override
+        public Map<String, String[]> getParameterMap() {
+            Map<String, String[]> parameterMap = super.getParameterMap();
+            if (parameterMap != null) {
+                for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
+                    String[] values = entry.getValue();
+                    if (values != null) {
+                        for (int i = 0; i < values.length; i++) {
+                            if (values[i] != null) {
+                                checkContent(values[i], "请求参数[" + entry.getKey() + "][" + i + "]");
+                            }
+                        }
+                    }
+                }
+            }
+            return parameterMap;
+        }
+
+        @Override
+        public ServletInputStream getInputStream() throws IOException {
+            if (body == null) {
+                return super.getInputStream();
+            }
+
+            final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body);
+
+            return new ServletInputStream() {
+                @Override
+                public boolean isFinished() {
+                    return byteArrayInputStream.available() == 0;
+                }
+
+                @Override
+                public boolean isReady() {
+                    return true;
+                }
+
+                @Override
+                public void setReadListener(ReadListener readListener) {
+                    // Not implemented
+                }
+
+                @Override
+                public int read() throws IOException {
+                    return byteArrayInputStream.read();
+                }
+            };
+        }
+
+        @Override
+        public BufferedReader getReader() throws IOException {
+            return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8));
+        }
+
+        /**
+         * 检查请求头
+         */
+        private void checkHeaders() {
+            Set<String> headersToCheck = new HashSet<>(DEFAULT_CHECK_HEADERS);
+            if (properties.getCheckHeaders() != null && !properties.getCheckHeaders().isEmpty()) {
+                headersToCheck.addAll(properties.getCheckHeaders());
+            }
+            
+            for (String headerName : headersToCheck) {
+                String headerValue = super.getHeader(headerName);
+                if (headerValue != null) {
+                    checkContent(headerValue, "请求头[" + headerName + "]");
+                }
+            }
+        }
+
+        /**
+         * 检查内容是否包含恶意代码
+         *
+         * @param content 待检查的内容
+         * @param location 内容来源位置(用于日志记录)
+         */
+        private void checkContent(String content, String location) {
+            // XSS 检测
+            if (properties.getXssEnabled() && SecurityUtils.containsXss(content)) {
+                throw new BadHttpRequestException("检测到 XSS 攻击尝试,位置: " + location);
+            }
+
+            // SQL 注入检测
+            if (properties.getSqlInjectionEnabled() && SecurityUtils.containsSqlInjection(content)) {
+                throw new BadHttpRequestException("检测到 SQL 注入尝试,位置: " + location);
+            }
+        }
+
+        /**
+         * 判断是否为 JSON 或 XML 请求
+         */
+        private boolean isJsonOrXmlRequest(HttpServletRequest request) {
+            String contentType = request.getHeader(HttpHeaders.CONTENT_TYPE);
+            if (contentType == null) {
+                return false;
+            }
+            return contentType.contains(MediaType.APPLICATION_JSON_VALUE)
+                    || contentType.contains(MediaType.APPLICATION_XML_VALUE)
+                    || contentType.contains(MediaType.TEXT_XML_VALUE);
+        }
+    }
+
+    @Override
+    public void init(FilterConfig filterConfig) throws ServletException {
+        log.info("XSS 和 SQL 注入防护过滤器已启动");
+    }
+
+    @Override
+    public void destroy() {
+        log.info("XSS 和 SQL 注入防护过滤器已销毁");
+    }
+}

+ 6 - 15
national-motion-module-system/national-motion-system-start/src/main/resources/application-dev.yml

@@ -413,21 +413,12 @@ http:
       after_inactivity: 30000
 
 # 异步线程池配置
-#async:
-#  thread-pool:
-#    core-pool-size: 10
-#    max-pool-size: 50
-#    queue-capacity: 1000
-#    keep-alive-seconds: 60
-#    thread-name-prefix: "business-async-"
-#    allow-core-thread-timeout: false
-#    await-termination-seconds: 30
 async:
   thread-pool:
-    core-pool-size: 15
-    max-pool-size: 51
-    queue-capacity: 1001
-    keep-alive-seconds: 61
-    thread-name-prefix: "business-async-1"
+    core-pool-size: 10
+    max-pool-size: 50
+    queue-capacity: 1000
+    keep-alive-seconds: 60
+    thread-name-prefix: "business-async-"
     allow-core-thread-timeout: false
-    await-termination-seconds: 31
+    await-termination-seconds: 30

+ 7 - 0
pom.xml

@@ -275,6 +275,13 @@
 				<version>3.8.0.2</version>
 			</dependency>
 
+            <!-- Jakarta Annotations API -->
+<!--            <dependency>-->
+<!--                <groupId>jakarta.annotation</groupId>-->
+<!--                <artifactId>jakarta.annotation-api</artifactId>-->
+<!--                <version>2.1.1</version>-->
+<!--            </dependency>-->
+
 			<dependency>
 				<groupId>org.redisson</groupId>
 				<artifactId>redisson</artifactId>