refactor(common-web): 优化 Web 基础配置与请求处理

- 修复可重复读取请求体包装器的 ServletInputStream 状态判断
  - 将 XSS 过滤器改为构造器注入配置,移除静态 Spring Bean 获取
  - 增加 CORS 配置属性,支持通过 web.cors.* 配置跨域规则
  - 优化请求日志参数处理,避免 JSON 解析异常影响主请求并限制日志长度
  - 收敛请求体解析异常响应,避免向前端暴露底层异常细节
This commit is contained in:
疯狂的狮子Li
2026-05-16 17:16:16 +08:00
parent 63cd82f4a8
commit f171ac03c4
9 changed files with 101 additions and 105 deletions

View File

@@ -32,7 +32,7 @@
</exclusion>
</exclusions>
</dependency>
<!-- web 容器使用 undertow 性能更强 -->
<!-- web 容器使用 jetty -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>

View File

@@ -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);
}
/**

View File

@@ -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);

View File

@@ -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<UndertowServletWebServerFactory> {
//
// /**
// * 自定义 Undertow 配置
// * <p>
// * 主要配置内容包括:
// * 1. 配置 WebSocket 部署信息
// * 2. 在虚拟线程模式下使用虚拟线程池
// * 3. 禁用不安全的 HTTP 方法,如 CONNECT、TRACE、TRACK
// * </p>
// *
// * @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);
// });
// });
// }
//
//}

View File

@@ -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<String> allowedOriginPatterns = new ArrayList<>(List.of("*"));
/**
* 允许的请求头。
*/
private List<String> allowedHeaders = new ArrayList<>(List.of("*"));
/**
* 允许的请求方法。
*/
private List<String> allowedMethods = new ArrayList<>(List.of("*"));
/**
* 预检请求缓存时间,单位秒。
*/
private Long maxAge = 1800L;
}

View File

@@ -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

View File

@@ -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<String> excludes = new ArrayList<>();
private final List<String> 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());
}
}
/**

View File

@@ -213,7 +213,7 @@ public class GlobalExceptionHandler {
public R<Void> 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<Void> 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, "请求参数格式错误");
}
/**

View File

@@ -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<StopWatch> 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<String, String[]> parameterMap = request.getParameterMap();
if (MapUtil.isNotEmpty(parameterMap)) {
Map<String, String[]> 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);
}
/**