Browse Source

feat(security): 添加 XSS 和 SQL 注入防护功能

- 新增安全过滤器配置,支持 XSS 和 SQL 注入检测
- 实现安全工具类 SecurityUtils,提供内容检测和清理方法
- 添加全局异常处理器,捕获并处理恶意请求异常
- 配置安全测试接口,用于验证防护功能
- 引入 Jakarta Annotations API 依赖
- 移除废弃的运营商登录接口
- 添加安全请求包装类,对请求参数、请求体和请求头进行安全检查
- 支持配置排除 URL 和自定义检查请求头
- 提供严格模式和宽松模式的 SQL 注入检测选项
wzq 4 days ago
parent
commit
8826f1538b

+ 7 - 0
pom.xml

@@ -69,6 +69,13 @@
             <scope>provided</scope>
         </dependency>
 
+        <!-- Jakarta Annotations API -->
+        <dependency>
+            <groupId>jakarta.annotation</groupId>
+            <artifactId>jakarta.annotation-api</artifactId>
+            <version>2.1.1</version>
+        </dependency>
+
         <dependency>
             <groupId>com.fasterxml.jackson.datatype</groupId>
             <artifactId>jackson-datatype-jsr310</artifactId>

+ 59 - 0
src/main/java/com/zsElectric/boot/auth/controller/SecurityTestController.java

@@ -0,0 +1,59 @@
+package com.zsElectric.boot.auth.controller;
+
+import com.zsElectric.boot.core.web.Result;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 安全测试控制器
+ * <p>
+ * 用于测试 XSS 和 SQL 注入防护功能
+ *
+ * @author zsElectric
+ */
+@Tag(name = "安全测试接口", description = "用于测试 XSS 和 SQL 注入防护功能")
+@RestController
+@RequestMapping("/api/v1/security/test")
+@RequiredArgsConstructor
+@Slf4j
+public class SecurityTestController {
+
+    @Operation(summary = "测试 XSS 防护", description = "测试 XSS 攻击防护功能")
+    @PostMapping("/xss")
+    public Result<String> testXss(@RequestBody TestRequest request) {
+        log.info("收到 XSS 测试请求: {}", request.getContent());
+        return Result.success("XSS 测试成功,内容: " + request.getContent());
+    }
+
+    @Operation(summary = "测试 SQL 注入防护", description = "测试 SQL 注入防护功能")
+    @PostMapping("/sql-injection")
+    public Result<String> testSqlInjection(@RequestBody TestRequest request) {
+        log.info("收到 SQL 注入测试请求: {}", request.getContent());
+        return Result.success("SQL 注入测试成功,内容: " + request.getContent());
+    }
+
+    @Operation(summary = "测试查询参数", description = "测试查询参数的 XSS 和 SQL 注入防护")
+    @GetMapping("/query")
+    public Result<String> testQueryParams(@RequestParam String param) {
+        log.info("收到查询参数测试请求: {}", param);
+        return Result.success("查询参数测试成功,内容: " + param);
+    }
+
+    /**
+     * 测试请求对象
+     */
+    public static class TestRequest {
+        private String content;
+
+        public String getContent() {
+            return content;
+        }
+
+        public void setContent(String content) {
+            this.content = content;
+        }
+    }
+}

+ 0 - 7
src/main/java/com/zsElectric/boot/auth/service/AuthService.java

@@ -66,13 +66,6 @@ public interface AuthService {
      */
     AuthenticationToken loginByWxMiniAppPhone(WxMiniAppPhoneLoginDTO loginDTO);
 
-    /**
-     * 运营商登录
-     *
-     * @return 访问令牌
-     */
-    AuthenticationToken OperatorLogin(String OperatorID,String OperatorSecret);
-
     /**
      * 发送短信验证码
      *

+ 0 - 9
src/main/java/com/zsElectric/boot/auth/service/impl/AuthServiceImpl.java

@@ -266,13 +266,4 @@ public class AuthServiceImpl implements AuthService {
 
         return token;
     }
-
-    @Override
-    public AuthenticationToken OperatorLogin(String OperatorID, String OperatorSecret) {
-
-
-
-        return null;
-    }
-
 }

+ 380 - 0
src/main/java/com/zsElectric/boot/common/util/SecurityUtils.java

@@ -0,0 +1,380 @@
+package com.zsElectric.boot.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;
+            }
+        }
+    }
+}

+ 42 - 0
src/main/java/com/zsElectric/boot/config/FilterConfig.java

@@ -0,0 +1,42 @@
+package com.zsElectric.boot.config;
+
+import com.zsElectric.boot.config.property.SecurityFilterProperties;
+import com.zsElectric.boot.core.filter.XssAndSqlInjectionFilter;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+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;
+    }
+}

+ 31 - 0
src/main/java/com/zsElectric/boot/config/SecurityUtilsConfig.java

@@ -0,0 +1,31 @@
+package com.zsElectric.boot.config;
+
+import com.zsElectric.boot.common.util.SecurityUtils;
+import com.zsElectric.boot.config.property.SecurityFilterProperties;
+import jakarta.annotation.PostConstruct;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 安全工具类配置
+ * <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() ? "严格模式" : "宽松模式");
+    }
+}

+ 48 - 0
src/main/java/com/zsElectric/boot/config/property/SecurityFilterProperties.java

@@ -0,0 +1,48 @@
+package com.zsElectric.boot.config.property;
+
+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
src/main/java/com/zsElectric/boot/core/exception/BadHttpRequestException.java

@@ -0,0 +1,22 @@
+package com.zsElectric.boot.core.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);
+    }
+}

+ 12 - 0
src/main/java/com/zsElectric/boot/core/exception/GlobalExceptionHandler.java

@@ -19,6 +19,7 @@ import org.springframework.validation.BindException;
 import org.springframework.web.bind.MethodArgumentNotValidException;
 import org.springframework.web.bind.MissingServletRequestParameterException;
 import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseBody;
 import org.springframework.web.bind.annotation.ResponseStatus;
 import org.springframework.web.bind.annotation.RestControllerAdvice;
 import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
@@ -52,6 +53,17 @@ public class GlobalExceptionHandler {
         return Result.failed(ResultCode.USER_REQUEST_PARAMETER_ERROR, msg);
     }
 
+    /**
+     * 处理恶意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.failed(ResultCode.USER_INPUT_CONTENT_ILLEGAL, ResultCode.USER_INPUT_CONTENT_ILLEGAL.getMsg());
+    }
+
     /**
      * 处理 @RequestParam 参数校验异常
      * <p>

+ 261 - 0
src/main/java/com/zsElectric/boot/core/filter/XssAndSqlInjectionFilter.java

@@ -0,0 +1,261 @@
+package com.zsElectric.boot.core.filter;
+
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.StrUtil;
+import com.zsElectric.boot.common.util.SecurityUtils;
+import com.zsElectric.boot.config.property.SecurityFilterProperties;
+import com.zsElectric.boot.core.exception.BadHttpRequestException;
+import jakarta.servlet.*;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletRequestWrapper;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+
+/**
+ * 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(
+            "Cookie",
+            "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 注入防护过滤器已销毁");
+    }
+}

+ 19 - 0
src/main/resources/application-dev.yml

@@ -122,6 +122,25 @@ security:
     - /dev/v1/linkData/notification_charge_order_info
     - /dev/v1/linkData/notification_stationStatus
 
+  # XSS 和 SQL 注入防护过滤器配置
+  filter:
+    # 是否启用 XSS 防护
+    xss-enabled: true
+    # 是否启用 SQL 注入防护
+    sql-injection-enabled: true
+    # SQL 注入检测严格模式
+    # true: 严格模式,可能误判一些正常输入
+    # false: 宽松模式,减少误判但可能漏掉一些攻击
+    sql-strict-mode: true
+    # 排除的 URL 路径(不进行安全检查,注意:默认已经排除了 /doc.html、/swagger-ui 等接口文档相关路径)
+    exclude-urls:
+      - /api/v1/auth/captcha  # 验证码接口
+    # 额外需要检查的请求头(默认已检查 User-Agent、Referer、X-Forwarded-For)
+    check-headers:
+      - Cookie
+      - Referer
+      - X-Forwarded-For
+
 okhttp:
   connect-timeout: 30s
   read-timeout: 120s