Browse Source

feat(firewall): 添加SQL注入拦截器防护

- 新增 SqlInjectionInterceptor 类,拦截所有请求参数防止SQL注入攻击
- 实现对URL参数和自定义请求头的SQL注入检测
- 排除标准HTTP请求头避免误拦截
- 编写 SqlInjectionConfiguration 配置类,注册并配置拦截器路径
- 拦截所有请求但排除静态资源、Swagger文档、druid监控、错误页面和WebSocket请求
- 捕获并返回安全风险提示,拦截非法请求,提升系统安全性
SheepHy 3 days ago
parent
commit
9df5655ae5

+ 5 - 1
national-motion-base-core/src/main/java/org/jeecg/common/util/SqlInjectionUtil.java

@@ -26,7 +26,7 @@ public class SqlInjectionUtil {
 	/**
 	 * 默认—sql注入关键词
 	 */
-	private final static String XSS_STR = "and |exec |peformance_schema|information_schema|extractvalue|updatexml|geohash|gtid_subset|gtid_subtract|insert |select |delete |update |drop |count |chr |mid |master |truncate |char |declare |;|or |+|--";
+	private final static String XSS_STR = "and |exec |peformance_schema|information_schema|extractvalue|updatexml|geohash|gtid_subset|gtid_subtract|insert |select |delete |update |drop |count |chr |mid |master |truncate |char |declare |;|or |+|--|union |jndi:|ldap:|rmi:";
 	/**
 	 * online报表专用—sql注入关键词
 	 */
@@ -62,6 +62,10 @@ public class SqlInjectionUtil {
 			"show\\s+databases",
 			"sleep\\(\\d*\\)",
 			"sleep\\(.*\\)",
+			"union\\s+select",
+			"\\$\\{jndi:",
+			"jndi:ldap",
+			"jndi:rmi",
 	};
 	/**
 	 * sql注释的正则

+ 73 - 0
national-motion-base-core/src/main/java/org/jeecg/config/firewall/interceptor/SqlInjectionInterceptor.java

@@ -1,10 +1,12 @@
 package org.jeecg.config.firewall.interceptor;
 
 import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
 import lombok.extern.slf4j.Slf4j;
 import org.jeecg.common.api.vo.Result;
 import org.jeecg.common.exception.JeecgSqlInjectionException;
 import org.jeecg.common.util.SqlInjectionUtil;
+import org.jeecg.config.sign.util.BodyReaderHttpServletRequestWrapper;
 import org.springframework.web.servlet.HandlerInterceptor;
 
 import javax.servlet.http.HttpServletRequest;
@@ -39,6 +41,9 @@ public class SqlInjectionInterceptor implements HandlerInterceptor {
             // 检查请求头参数(排除常见的安全请求头)
             checkHeaders(request);
             
+            // 检查POST请求的JSON Body
+            checkRequestBody(request);
+            
             return true;
         } catch (JeecgSqlInjectionException e) {
             log.error("检测到SQL注入攻击! URI: {}, 错误信息: {}", request.getRequestURI(), e.getMessage());
@@ -91,6 +96,74 @@ public class SqlInjectionInterceptor implements HandlerInterceptor {
         }
     }
 
+    /**
+     * 检查POST请求的JSON Body
+     */
+    private void checkRequestBody(HttpServletRequest request) {
+        // 只检查POST请求
+        if (!"POST".equalsIgnoreCase(request.getMethod())) {
+            return;
+        }
+        
+        // 如果是BodyReaderHttpServletRequestWrapper,说明已经被包装过,可以读取body
+        if (request instanceof BodyReaderHttpServletRequestWrapper) {
+            BodyReaderHttpServletRequestWrapper wrapper = (BodyReaderHttpServletRequestWrapper) request;
+            String bodyString = wrapper.getBodyString(request);
+            
+            log.info("[SQL注入检查] 开始检查POST请求Body, URI: {}, Body: {}", request.getRequestURI(), bodyString);
+            
+            if (bodyString != null && !bodyString.trim().isEmpty()) {
+                try {
+                    // 尝试解析为JSON
+                    JSONObject jsonObject = JSON.parseObject(bodyString);
+                    if (jsonObject != null) {
+                        // 递归检查JSON中的所有字符串值
+                        checkJsonValues(jsonObject);
+                    }
+                } catch (Exception e) {
+                    // 如果不是JSON格式,直接检查整个body字符串
+                    log.debug("请求body不是JSON格式,直接检查内容");
+                    SqlInjectionUtil.filterContent(bodyString, null);
+                }
+            }
+        } else {
+            log.warn("[SQL注入检查] POST请求未被BodyReaderHttpServletRequestWrapper包装! URI: {}, Request类型: {}", 
+                    request.getRequestURI(), request.getClass().getName());
+        }
+    }
+
+    /**
+     * 递归检查JSON中的所有字符串值
+     */
+    private void checkJsonValues(Object obj) {
+        if (obj == null) {
+            return;
+        }
+        
+        if (obj instanceof String) {
+            String value = (String) obj;
+            if (!value.trim().isEmpty()) {
+                log.debug("[SQL注入检查] 检查字符串值: {}", value);
+                SqlInjectionUtil.filterContent(value, null);
+            }
+        } else if (obj instanceof JSONObject) {
+            JSONObject jsonObject = (JSONObject) obj;
+            for (Map.Entry<String, Object> entry : jsonObject.entrySet()) {
+                log.debug("[SQL注入检查] 检查JSON字段: {} = {}", entry.getKey(), entry.getValue());
+                checkJsonValues(entry.getValue());
+            }
+        } else if (obj instanceof Map) {
+            Map<?, ?> map = (Map<?, ?>) obj;
+            for (Object value : map.values()) {
+                checkJsonValues(value);
+            }
+        } else if (obj instanceof Iterable) {
+            for (Object item : (Iterable<?>) obj) {
+                checkJsonValues(item);
+            }
+        }
+    }
+
     /**
      * 判断是否为标准HTTP请求头
      */

+ 4 - 12
national-motion-base-core/src/main/java/org/jeecg/config/sign/interceptor/SignAuthConfiguration.java

@@ -52,18 +52,10 @@ public class SignAuthConfiguration implements WebMvcConfigurer {
         FilterRegistrationBean registration = new FilterRegistrationBean();
         registration.setFilter(requestBodyReserveFilter());
         registration.setName("requestBodyReserveFilter");
-        //------------------------------------------------------------
-        //查询需要进行签名拦截的接口 signUrls
-        String signUrls = jeecgBaseConfig.getSignUrls();
-        String[] signUrlsArray = null;
-        if (StringUtils.isNotBlank(signUrls)) {
-            signUrlsArray = signUrls.split(",");
-        } else {
-            signUrlsArray = PathMatcherUtil.SIGN_URL_LIST;
-        }
-        //------------------------------------------------------------
-        // 建议此处只添加post请求地址而不是所有的都需要走过滤器
-        registration.addUrlPatterns(signUrlsArray);
+        // 修改为拦截所有POST请求,使SQL注入拦截器能读取body
+        registration.addUrlPatterns("/*");
+        // 设置最高优先级,确保在所有Filter(包括Shiro)之前执行
+        registration.setOrder(Integer.MIN_VALUE);
         return registration;
     }
     //update-end-author:taoyan date:20220427 for: issues/I53J5E post请求X_SIGN签名拦截校验后报错, request body 为空

+ 85 - 28
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/app/service/impl/OrderServiceImpl.java

@@ -1145,39 +1145,96 @@ public class OrderServiceImpl extends ServiceImpl<AppOrderMapper, AppOrder> impl
                     AppSite appSite = appSiteMapper.selectById(appCoursesMapper.selectById(appOrderProInfo.getProductId()).getAddressSiteId());
                     if (null != appSite &&
                             appSite.getType() == 0) {
-                        AppOrderProInfo proInfo1 = appOrderProInfoMapper.selectOne(Wrappers.<AppOrderProInfo>lambdaQuery()
+                        // 查询该用户所有待使用的课程订单,合并时间范围
+                        List<AppOrderProInfo> allCourseOrders = appOrderProInfoMapper.selectList(Wrappers.<AppOrderProInfo>lambdaQuery()
                                 .eq(AppOrderProInfo::getFamilyUserId, appOrderProInfo.getFamilyUserId())
                                 .eq(AppOrderProInfo::getOrderStatus, 1)
                                 .eq(AppOrderProInfo::getType, 5)
-                                .orderByDesc(AppOrderProInfo::getExpireTime)
-                                .last("limit 1"));
-                        AppOrderProInfo proInfo2 = appOrderProInfoMapper.selectOne(Wrappers.<AppOrderProInfo>lambdaQuery()
-                                .eq(AppOrderProInfo::getFamilyUserId, appOrderProInfo.getFamilyUserId())
-                                .eq(AppOrderProInfo::getOrderStatus, 1)
-                                .eq(AppOrderProInfo::getType, 5)
-                                .orderByAsc(AppOrderProInfo::getExpireTime)
-                                .last("limit 1"));
+                                .isNotNull(AppOrderProInfo::getFrameTimeStr));
+                        
                         FamilyMembers familyMembers = familyMembersMapper.selectById(appOrderProInfo.getFamilyUserId());
-                        for (AppDevice appDevice : appDeviceMapper.selectList(Wrappers.<AppDevice>lambdaQuery().eq(AppDevice::getSiteId, appSite.getId()))) {
-                            if (null != appDevice && proInfo1 != null && proInfo2 != null) {
-                                JsonObject addUserJson = JsonParser.parseString(addUser(new DateTime(proInfo2.getExpireTime()),
-                                        appDevice.getDeviceSerial(),
-                                        appOrderProInfo.getUserName(),
-                                        familyMembers.getId(), new DateTime(proInfo1.getExpireTime()), appOrderProInfo.getFrameTimeStr())).getAsJsonObject();
-                                JsonObject addFaceJson = JsonParser.parseString(addFace(appDevice.getDeviceSerial(), familyMembers.getId(),
-                                        familyMembers.getRealNameImg())).getAsJsonObject();
-                                if (addUserJson.get("code").getAsInt() != 0 && addFaceJson.get("code").getAsInt() != 0) {
-                                    throw new JeecgBootException("设备录入用户信息失败!请联系管理员");
+                        
+                        if (CollUtil.isNotEmpty(allCourseOrders)) {
+                            // 解析所有订单的frameTimeStr,找出最早和最晚的日期
+                            LocalDate earliestStartDate = null;
+                            LocalDate latestEndDate = null;
+                            
+                            for (AppOrderProInfo courseOrder : allCourseOrders) {
+                                String frameTimeStr = courseOrder.getFrameTimeStr();
+                                if (frameTimeStr != null && !frameTimeStr.isEmpty()) {
+                                    try {
+                                        // 解析 "yyyy-MM-dd-MM-dd" 格式
+                                        String[] parts = frameTimeStr.split("-");
+                                        if (parts.length == 5) {
+                                            String startDateStr = parts[0] + "-" + parts[1] + "-" + parts[2];
+                                            String endDateStr = parts[0] + "-" + parts[3] + "-" + parts[4];
+                                            
+                                            LocalDate startDate = LocalDate.parse(startDateStr);
+                                            LocalDate endDate = LocalDate.parse(endDateStr);
+                                            
+                                            // 找出最早的开始日期
+                                            if (earliestStartDate == null || startDate.isBefore(earliestStartDate)) {
+                                                earliestStartDate = startDate;
+                                            }
+                                            // 找出最晚的结束日期
+                                            if (latestEndDate == null || endDate.isAfter(latestEndDate)) {
+                                                latestEndDate = endDate;
+                                            }
+                                        }
+                                    } catch (Exception e) {
+                                        // 解析失败,跳过该订单
+                                        log.error("解析课程订单frameTimeStr失败: {}", frameTimeStr, e);
+                                    }
                                 }
-                            }else if (null != appDevice) {
-                                JsonObject addUserJson = JsonParser.parseString(addUser(new Date(),
-                                        appDevice.getDeviceSerial(),
-                                        appOrderProInfo.getUserName(),
-                                        familyMembers.getId(), new DateTime(appOrderProInfo.getExpireTime()), appOrderProInfo.getFrameTimeStr())).getAsJsonObject();
-                                JsonObject addFaceJson = JsonParser.parseString(addFace(appDevice.getDeviceSerial(), familyMembers.getId(),
-                                        familyMembers.getRealNameImg())).getAsJsonObject();
-                                if (addUserJson.get("code").getAsInt() != 0 && addFaceJson.get("code").getAsInt() != 0) {
-                                    throw new JeecgBootException("设备录入用户信息失败!请联系管理员");
+                            }
+                            
+                            // 如果找到了有效的日期范围,合并为一个frameTimeStr
+                            if (earliestStartDate != null && latestEndDate != null) {
+                                // 构造合并后的frameTimeStr: "yyyy-MM-dd-MM-dd"
+                                String mergedFrameTimeStr = earliestStartDate.getYear() + "-" +
+                                        String.format("%02d", earliestStartDate.getMonthValue()) + "-" +
+                                        String.format("%02d", earliestStartDate.getDayOfMonth()) + "-" +
+                                        String.format("%02d", latestEndDate.getMonthValue()) + "-" +
+                                        String.format("%02d", latestEndDate.getDayOfMonth());
+                                
+                                // 使用合并后的时间范围更新门禁权限
+                                for (AppDevice appDevice : appDeviceMapper.selectList(Wrappers.<AppDevice>lambdaQuery().eq(AppDevice::getSiteId, appSite.getId()))) {
+                                    if (null != appDevice) {
+                                        JsonObject addUserJson = JsonParser.parseString(addUser(
+                                                new Date(),  // inputDate可以传任意值,会被frameTimeStr覆盖
+                                                appDevice.getDeviceSerial(),
+                                                appOrderProInfo.getUserName(),
+                                                familyMembers.getId(),
+                                                null,  // endDate会被frameTimeStr覆盖
+                                                mergedFrameTimeStr)).getAsJsonObject();
+                                        JsonObject addFaceJson = JsonParser.parseString(addFace(
+                                                appDevice.getDeviceSerial(),
+                                                familyMembers.getId(),
+                                                familyMembers.getRealNameImg())).getAsJsonObject();
+                                        if (addUserJson.get("code").getAsInt() != 0 && addFaceJson.get("code").getAsInt() != 0) {
+                                            throw new JeecgBootException("设备录入用户信息失败!请联系管理员");
+                                        }
+                                    }
+                                }
+                            }
+                        } else if (null != appOrderProInfo.getFrameTimeStr()) {
+                            // 如果只有当前订单,直接使用当前订单的frameTimeStr
+                            for (AppDevice appDevice : appDeviceMapper.selectList(Wrappers.<AppDevice>lambdaQuery().eq(AppDevice::getSiteId, appSite.getId()))) {
+                                if (null != appDevice) {
+                                    JsonObject addUserJson = JsonParser.parseString(addUser(
+                                            new Date(),
+                                            appDevice.getDeviceSerial(),
+                                            appOrderProInfo.getUserName(),
+                                            familyMembers.getId(),
+                                            null,
+                                            appOrderProInfo.getFrameTimeStr())).getAsJsonObject();
+                                    JsonObject addFaceJson = JsonParser.parseString(addFace(
+                                            appDevice.getDeviceSerial(),
+                                            familyMembers.getId(),
+                                            familyMembers.getRealNameImg())).getAsJsonObject();
+                                    if (addUserJson.get("code").getAsInt() != 0 && addFaceJson.get("code").getAsInt() != 0) {
+                                        throw new JeecgBootException("设备录入用户信息失败!请联系管理员");
+                                    }
                                 }
                             }
                         }

+ 59 - 13
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/hikiot/HikiotTool.java

@@ -198,29 +198,75 @@ public class HikiotTool {
      * @Author SheepHy
      * @Description 新增/修改人员
      * @Date 9:35 2025/8/15
+     * @param inputDate 开始日期(必填)
+     * @param deviceSerial 设备序列号(必填)
+     * @param name 用户名称(必填)
+     * @param employeeNo 员工编号(必填)
+     * @param endDate 结束日期(选填,不传则使用开始日期)
+     * @param frameTimeStr 时间段(选填,支持两种格式)
+     *                     格式1: "HH:mm:ss-HH:mm:ss" 如"18:00:00-22:00:00" - 场地预约订单(type=0)的当日时间段
+     *                            示例:2025-11-05的18:00-22:00,结果为 beginTime:2025-11-05T18:00:00, endTime:2025-11-05T22:00:00
+     *                     格式2: "yyyy-MM-dd-MM-dd" 如"2025-11-02-11-02" - 场地预约订单(type=5)的课程日期范围
+     *                            示例:2025-11-02到2025-11-02,结果为 beginTime:2025-11-02T00:00:00, endTime:2025-11-02T23:59:59
+     *                     注意:纯课程订单通常不传此参数(传null),直接使用inputDate和endDate即可
+     * @return 海康接口返回结果
      **/
     public static String addUser(Date inputDate,String deviceSerial,String name,String employeeNo,Date endDate, String frameTimeStr){
         LocalDateTime now = inputDate.toInstant()
                 .atZone(ZoneId.systemDefault())
                 .toLocalDateTime();
-        LocalDateTime startOfDay = now.with(LocalTime.MIN); // 00:00
-        LocalDateTime endOfDay = now.with(LocalTime.MAX);
+        // 默认开始时间为当天00:00:00
+        LocalDateTime startOfDay = now.with(LocalTime.MIN); // 00:00:00
+        // 默认结束时间为当天23:59:59
+        LocalDateTime endOfDay = now.with(LocalTime.MAX);   // 23:59:59.999999999
+        
+        // 如果指定了结束日期,则使用该日期(保持23:59:59)
         if(null != endDate){
             LocalDateTime outputDate = endDate.toInstant()
                     .atZone(ZoneId.systemDefault())
-                    .toLocalDateTime()
-                    .plusDays(1);
+                    .toLocalDateTime();
             endOfDay = outputDate.with(LocalTime.MAX);
         }
-        if(null != frameTimeStr){
-            // 解析frameTimeStr格式为 "HH:mm:ss-HH:mm:ss"
-            String[] times = frameTimeStr.split("-");
-            if(times.length == 2) {
-                LocalTime customStartTime = LocalTime.parse(times[0]);
-                LocalTime customEndTime = LocalTime.parse(times[1]);
-                // 将自定义时间应用到有效时间范围
-                startOfDay = now.with(customStartTime);
-                endOfDay = now.with(customEndTime);
+        
+        // 处理frameTimeStr参数,支持两种格式
+        if(null != frameTimeStr && !frameTimeStr.isEmpty()){
+            String[] parts = frameTimeStr.split("-");
+            
+            // 格式1: "HH:mm:ss-HH:mm:ss" (场地预约type=0,当日时间段,例如: "18:00:00-22:00:00")
+            //        按'-'分割后长度为2,且包含':'
+            //        注意:此格式只应用时间到当天,endTime使用当天的结束时间
+            // 格式2: "yyyy-MM-dd-MM-dd" (场地预约type=5课程,日期范围,例如: "2025-11-02-11-02")
+            //        按'-'分割后长度为5 (2025, 11, 02, 11, 02)
+            
+            if(parts.length == 2 && parts[0].contains(":")) {
+                // 格式1: 场地预约type=0的当日时间段 "HH:mm:ss-HH:mm:ss"
+                // 示例: "18:00:00-22:00:00" 表示当天18:00到22:00
+                try {
+                    LocalTime customStartTime = LocalTime.parse(parts[0]);
+                    LocalTime customEndTime = LocalTime.parse(parts[1]);
+                    // 开始时间和结束时间都应用到同一天(inputDate)
+                    startOfDay = now.with(customStartTime);  // 2025-11-05T18:00:00
+                    endOfDay = now.with(customEndTime);      // 2025-11-05T22:00:00
+                } catch (Exception e) {
+                    // 时间格式解析失败,使用默认全天时间
+                }
+            } else if(parts.length == 5) {
+                // 格式2: 场地预约type=5课程的日期范围 "yyyy-MM-dd-MM-dd"
+                // 示例: "2025-11-02-11-05" 表示从2025-11-02到2025-11-05全天
+                try {
+                    // 起始日期: parts[0]-parts[1]-parts[2] = "2025-11-02"
+                    String startDateStr = parts[0] + "-" + parts[1] + "-" + parts[2];
+                    // 结束日期: parts[0]-parts[3]-parts[4] = "2025-11-05" (使用同一年份)
+                    String endDateStr = parts[0] + "-" + parts[3] + "-" + parts[4];
+                    
+                    LocalDate parsedStartDate = LocalDate.parse(startDateStr);
+                    LocalDate parsedEndDate = LocalDate.parse(endDateStr);
+                    
+                    startOfDay = parsedStartDate.atTime(LocalTime.MIN);  // 2025-11-02T00:00:00
+                    endOfDay = parsedEndDate.atTime(LocalTime.MAX);      // 2025-11-05T23:59:59
+                } catch (Exception e) {
+                    // 日期格式解析失败,使用默认全天时间
+                }
             }
         }
         AddUserRequestDTO addUserRequestDTO = new AddUserRequestDTO();