diff --git a/ruoyi-common/ruoyi-common-web/pom.xml b/ruoyi-common/ruoyi-common-web/pom.xml index 65965eec0..cb6e35cd7 100644 --- a/ruoyi-common/ruoyi-common-web/pom.xml +++ b/ruoyi-common/ruoyi-common-web/pom.xml @@ -32,7 +32,7 @@ - + org.springframework.boot spring-boot-starter-jetty diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/FilterConfig.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/FilterConfig.java index b095851d3..b7de20e86 100644 --- a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/FilterConfig.java +++ b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/FilterConfig.java @@ -33,8 +33,8 @@ public class FilterConfig { order = FilterRegistrationBean.HIGHEST_PRECEDENCE + 1, dispatcherTypes = DispatcherType.REQUEST ) - public XssFilter xssFilter() { - return new XssFilter(); + public XssFilter xssFilter(XssProperties xssProperties) { + return new XssFilter(xssProperties); } /** diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/ResourcesConfig.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/ResourcesConfig.java index c73309ba3..00f8dce5b 100644 --- a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/ResourcesConfig.java +++ b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/ResourcesConfig.java @@ -5,16 +5,17 @@ import cn.hutool.core.date.DateUtil; import org.dromara.common.core.utils.ObjectUtils; import org.dromara.common.json.enhance.JsonValueEnhancer; import org.dromara.common.web.advice.ResponseEnhancementAdvice; +import org.dromara.common.web.config.properties.CorsProperties; import org.dromara.common.web.handler.GlobalExceptionHandler; import org.dromara.common.web.interceptor.PlusWebInvokeTimeInterceptor; import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.format.FormatterRegistry; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; -import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.Date; @@ -25,6 +26,7 @@ import java.util.Date; * @author Lion Li */ @AutoConfiguration +@EnableConfigurationProperties(CorsProperties.class) public class ResourcesConfig implements WebMvcConfigurer { /** @@ -55,32 +57,19 @@ public class ResourcesConfig implements WebMvcConfigurer { }); } - /** - * 注册静态资源处理器。 - * - * @param registry 资源处理器注册表 - */ - @Override - public void addResourceHandlers(ResourceHandlerRegistry registry) { - } - /** * 跨域配置 * * @return 全局 Cors 过滤器 */ @Bean - public CorsFilter corsFilter() { + public CorsFilter corsFilter(CorsProperties corsProperties) { CorsConfiguration config = new CorsConfiguration(); - config.setAllowCredentials(true); - // 设置访问源地址 - config.addAllowedOriginPattern("*"); - // 设置访问源请求头 - config.addAllowedHeader("*"); - // 设置访问源请求方法 - config.addAllowedMethod("*"); - // 有效期 1800秒 - config.setMaxAge(1800L); + config.setAllowCredentials(corsProperties.getAllowCredentials()); + config.setAllowedOriginPatterns(corsProperties.getAllowedOriginPatterns()); + config.setAllowedHeaders(corsProperties.getAllowedHeaders()); + config.setAllowedMethods(corsProperties.getAllowedMethods()); + config.setMaxAge(corsProperties.getMaxAge()); // 添加映射路径,拦截一切请求 UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/UndertowConfig.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/UndertowConfig.java deleted file mode 100644 index 8179bea85..000000000 --- a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/UndertowConfig.java +++ /dev/null @@ -1,63 +0,0 @@ -//package org.dromara.common.web.config; -// -//import io.undertow.server.DefaultByteBufferPool; -//import io.undertow.server.handlers.DisallowedMethodsHandler; -//import io.undertow.util.HttpString; -//import io.undertow.websockets.jsr.WebSocketDeploymentInfo; -//import org.dromara.common.core.utils.SpringUtils; -//import org.springframework.boot.autoconfigure.AutoConfiguration; -//import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory; -//import org.springframework.boot.web.server.WebServerFactoryCustomizer; -//import org.springframework.core.task.VirtualThreadTaskExecutor; -// -///** -// * Undertow 自定义配置 -// * -// * @author Lion Li -// */ -//@AutoConfiguration -//public class UndertowConfig implements WebServerFactoryCustomizer { -// -// /** -// * 自定义 Undertow 配置 -// *

-// * 主要配置内容包括: -// * 1. 配置 WebSocket 部署信息 -// * 2. 在虚拟线程模式下使用虚拟线程池 -// * 3. 禁用不安全的 HTTP 方法,如 CONNECT、TRACE、TRACK -// *

-// * -// * @param factory Undertow 的 Web 服务器工厂 -// */ -// @Override -// public void customize(UndertowServletWebServerFactory factory) { -// factory.addDeploymentInfoCustomizers(deploymentInfo -> { -// // 配置 WebSocket 部署信息,设置 WebSocket 使用的缓冲区池 -// WebSocketDeploymentInfo webSocketDeploymentInfo = new WebSocketDeploymentInfo(); -// webSocketDeploymentInfo.setBuffers(new DefaultByteBufferPool(true, 1024)); -// deploymentInfo.addServletContextAttribute("io.undertow.websockets.jsr.WebSocketDeploymentInfo", webSocketDeploymentInfo); -// -// // 如果启用了虚拟线程,配置 Undertow 使用虚拟线程池 -// if (SpringUtils.isVirtual()) { -// // 创建虚拟线程池,线程池前缀为 "undertow-" -// VirtualThreadTaskExecutor executor = new VirtualThreadTaskExecutor("undertow-"); -// // 设置虚拟线程池为执行器和异步执行器 -// deploymentInfo.setExecutor(executor); -// deploymentInfo.setAsyncExecutor(executor); -// } -// -// // 配置禁止某些不安全的 HTTP 方法(如 CONNECT、TRACE、TRACK) -// deploymentInfo.addInitialHandlerChainWrapper(handler -> { -// // 禁止三个方法 CONNECT/TRACE/TRACK 也是不安全的 避免爬虫骚扰 -// HttpString[] disallowedHttpMethods = { -// HttpString.tryFromString("CONNECT"), -// HttpString.tryFromString("TRACE"), -// HttpString.tryFromString("TRACK") -// }; -// // 使用 DisallowedMethodsHandler 拦截并拒绝这些方法的请求 -// return new DisallowedMethodsHandler(handler, disallowedHttpMethods); -// }); -// }); -// } -// -//} diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/properties/CorsProperties.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/properties/CorsProperties.java new file mode 100644 index 000000000..8b30dcc9c --- /dev/null +++ b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/properties/CorsProperties.java @@ -0,0 +1,41 @@ +package org.dromara.common.web.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.ArrayList; +import java.util.List; + +/** + * 跨域配置属性。 + */ +@Data +@ConfigurationProperties(prefix = "web.cors") +public class CorsProperties { + + /** + * 是否允许携带凭证。 + */ + private Boolean allowCredentials = true; + + /** + * 允许的来源匹配规则。 + */ + private List allowedOriginPatterns = new ArrayList<>(List.of("*")); + + /** + * 允许的请求头。 + */ + private List allowedHeaders = new ArrayList<>(List.of("*")); + + /** + * 允许的请求方法。 + */ + private List allowedMethods = new ArrayList<>(List.of("*")); + + /** + * 预检请求缓存时间,单位秒。 + */ + private Long maxAge = 1800L; + +} diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/filter/RepeatedlyRequestWrapper.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/filter/RepeatedlyRequestWrapper.java index a282a9bfc..a5e14451c 100644 --- a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/filter/RepeatedlyRequestWrapper.java +++ b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/filter/RepeatedlyRequestWrapper.java @@ -13,6 +13,7 @@ import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; /** * 构建可重复读取输入流的请求包装器,缓存请求体以支持多次读取。 @@ -45,7 +46,7 @@ public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper { */ @Override public BufferedReader getReader() throws IOException { - return new BufferedReader(new InputStreamReader(getInputStream())); + return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8)); } /** @@ -65,17 +66,17 @@ public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper { @Override public int available() throws IOException { - return body.length; + return bais.available(); } @Override public boolean isFinished() { - return false; + return bais.available() == 0; } @Override public boolean isReady() { - return false; + return true; } @Override diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/filter/XssFilter.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/filter/XssFilter.java index b63af29d7..6104f043b 100644 --- a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/filter/XssFilter.java +++ b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/filter/XssFilter.java @@ -1,6 +1,6 @@ package org.dromara.common.web.filter; -import org.dromara.common.core.utils.SpringUtils; +import lombok.RequiredArgsConstructor; import org.dromara.common.core.utils.StringUtils; import org.dromara.common.web.config.properties.XssProperties; import org.springframework.http.HttpMethod; @@ -17,11 +17,14 @@ import java.util.List; * * @author ruoyi */ +@RequiredArgsConstructor public class XssFilter implements Filter { /** * 跳过 XSS 过滤的请求路径集合。 */ - public List excludes = new ArrayList<>(); + private final List excludes = new ArrayList<>(); + + private final XssProperties properties; /** * 初始化过滤器并加载配置中的排除路径。 @@ -31,8 +34,9 @@ public class XssFilter implements Filter { */ @Override public void init(FilterConfig filterConfig) throws ServletException { - XssProperties properties = SpringUtils.getBean(XssProperties.class); - excludes.addAll(properties.getExcludeUrls()); + if (properties.getExcludeUrls() != null) { + excludes.addAll(properties.getExcludeUrls()); + } } /** diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/handler/GlobalExceptionHandler.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/handler/GlobalExceptionHandler.java index b69761af2..f75ffbc24 100644 --- a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/handler/GlobalExceptionHandler.java +++ b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/handler/GlobalExceptionHandler.java @@ -213,7 +213,7 @@ public class GlobalExceptionHandler { public R handleJsonParseException(JsonParseException e, HttpServletRequest request) { String requestURI = request.getRequestURI(); log.error("请求地址'{}' 发生 JSON 解析异常: {}", requestURI, e.getMessage()); - return R.fail(HttpStatus.HTTP_BAD_REQUEST, "请求数据格式错误(JSON 解析失败):" + e.getMessage()); + return R.fail(HttpStatus.HTTP_BAD_REQUEST, "请求数据格式错误"); } /** @@ -222,7 +222,7 @@ public class GlobalExceptionHandler { @ExceptionHandler(HttpMessageNotReadableException.class) public R handleHttpMessageNotReadableException(HttpMessageNotReadableException e, HttpServletRequest request) { log.error("请求地址'{}', 参数解析失败: {}", request.getRequestURI(), e.getMessage()); - return R.fail(HttpStatus.HTTP_BAD_REQUEST, "请求参数格式错误:" + e.getMostSpecificCause().getMessage()); + return R.fail(HttpStatus.HTTP_BAD_REQUEST, "请求参数格式错误"); } /** diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/interceptor/PlusWebInvokeTimeInterceptor.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/interceptor/PlusWebInvokeTimeInterceptor.java index 6e96d5296..a56153a65 100644 --- a/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/interceptor/PlusWebInvokeTimeInterceptor.java +++ b/ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/interceptor/PlusWebInvokeTimeInterceptor.java @@ -14,7 +14,6 @@ import org.dromara.common.json.utils.JsonUtils; import org.dromara.common.web.filter.RepeatedlyRequestWrapper; import org.springframework.http.MediaType; import org.springframework.web.servlet.HandlerInterceptor; -import org.springframework.web.servlet.ModelAndView; import tools.jackson.databind.JsonNode; import tools.jackson.databind.json.JsonMapper; import tools.jackson.databind.node.ArrayNode; @@ -36,6 +35,11 @@ public class PlusWebInvokeTimeInterceptor implements HandlerInterceptor { private final static ThreadLocal KEY_CACHE = new ThreadLocal<>(); + /** + * 请求参数日志最大长度。 + */ + private static final int MAX_PARAM_LOG_LENGTH = 4000; + /** * 请求进入控制器前记录入参并启动耗时统计。 * @@ -54,20 +58,17 @@ public class PlusWebInvokeTimeInterceptor implements HandlerInterceptor { if (request instanceof RepeatedlyRequestWrapper) { jsonParam = IoUtil.read(request.getReader()); if (StringUtils.isNotBlank(jsonParam)) { - JsonMapper jsonMapper = JsonUtils.getJsonMapper(); - JsonNode rootNode = jsonMapper.readTree(jsonParam); - removeSensitiveFields(rootNode, SystemConstants.EXCLUDE_PROPERTIES); - jsonParam = rootNode.toString(); + jsonParam = sanitizeJsonParam(jsonParam); } } - log.info("[PLUS]开始请求 => URL[{}],参数类型[json],参数:[{}]", url, jsonParam); + log.info("[PLUS]开始请求 => URL[{}],参数类型[json],参数:[{}]", url, limit(jsonParam)); } else { Map parameterMap = request.getParameterMap(); if (MapUtil.isNotEmpty(parameterMap)) { Map map = new LinkedHashMap<>(parameterMap); MapUtil.removeAny(map, SystemConstants.EXCLUDE_PROPERTIES); String parameters = JsonUtils.toJsonString(map); - log.info("[PLUS]开始请求 => URL[{}],参数类型[param],参数:[{}]", url, parameters); + log.info("[PLUS]开始请求 => URL[{}],参数类型[param],参数:[{}]", url, limit(parameters)); } else { log.info("[PLUS]开始请求 => URL[{}],无参数", url); } @@ -110,9 +111,32 @@ public class PlusWebInvokeTimeInterceptor implements HandlerInterceptor { } } - @Override - public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { + /** + * 清洗 JSON 请求参数日志,解析失败时不影响主请求。 + * + * @param jsonParam 原始 JSON 字符串 + * @return 清洗后的参数日志 + */ + private String sanitizeJsonParam(String jsonParam) { + try { + JsonMapper jsonMapper = JsonUtils.getJsonMapper(); + JsonNode rootNode = jsonMapper.readTree(jsonParam); + removeSensitiveFields(rootNode, SystemConstants.EXCLUDE_PROPERTIES); + return rootNode.toString(); + } catch (Exception e) { + log.debug("[PLUS]请求参数 JSON 解析失败,跳过结构化脱敏: {}", e.getMessage()); + return jsonParam; + } + } + /** + * 限制日志字段长度。 + * + * @param value 原始字符串 + * @return 截断后的字符串 + */ + private String limit(String value) { + return StringUtils.substring(value, 0, MAX_PARAM_LOG_LENGTH); } /**