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
+1 -1
View File
@@ -32,7 +32,7 @@
</exclusion> </exclusion>
</exclusions> </exclusions>
</dependency> </dependency>
<!-- web 容器使用 undertow 性能更强 --> <!-- web 容器使用 jetty -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId> <artifactId>spring-boot-starter-jetty</artifactId>
@@ -33,8 +33,8 @@ public class FilterConfig {
order = FilterRegistrationBean.HIGHEST_PRECEDENCE + 1, order = FilterRegistrationBean.HIGHEST_PRECEDENCE + 1,
dispatcherTypes = DispatcherType.REQUEST dispatcherTypes = DispatcherType.REQUEST
) )
public XssFilter xssFilter() { public XssFilter xssFilter(XssProperties xssProperties) {
return new XssFilter(); return new XssFilter(xssProperties);
} }
/** /**
@@ -5,16 +5,17 @@ import cn.hutool.core.date.DateUtil;
import org.dromara.common.core.utils.ObjectUtils; import org.dromara.common.core.utils.ObjectUtils;
import org.dromara.common.json.enhance.JsonValueEnhancer; import org.dromara.common.json.enhance.JsonValueEnhancer;
import org.dromara.common.web.advice.ResponseEnhancementAdvice; 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.handler.GlobalExceptionHandler;
import org.dromara.common.web.interceptor.PlusWebInvokeTimeInterceptor; import org.dromara.common.web.interceptor.PlusWebInvokeTimeInterceptor;
import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.format.FormatterRegistry; import org.springframework.format.FormatterRegistry;
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter; import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 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 org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.Date; import java.util.Date;
@@ -25,6 +26,7 @@ import java.util.Date;
* @author Lion Li * @author Lion Li
*/ */
@AutoConfiguration @AutoConfiguration
@EnableConfigurationProperties(CorsProperties.class)
public class ResourcesConfig implements WebMvcConfigurer { public class ResourcesConfig implements WebMvcConfigurer {
/** /**
@@ -55,32 +57,19 @@ public class ResourcesConfig implements WebMvcConfigurer {
}); });
} }
/**
* 注册静态资源处理器。
*
* @param registry 资源处理器注册表
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
}
/** /**
* 跨域配置 * 跨域配置
* *
* @return 全局 Cors 过滤器 * @return 全局 Cors 过滤器
*/ */
@Bean @Bean
public CorsFilter corsFilter() { public CorsFilter corsFilter(CorsProperties corsProperties) {
CorsConfiguration config = new CorsConfiguration(); CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true); config.setAllowCredentials(corsProperties.getAllowCredentials());
// 设置访问源地址 config.setAllowedOriginPatterns(corsProperties.getAllowedOriginPatterns());
config.addAllowedOriginPattern("*"); config.setAllowedHeaders(corsProperties.getAllowedHeaders());
// 设置访问源请求头 config.setAllowedMethods(corsProperties.getAllowedMethods());
config.addAllowedHeader("*"); config.setMaxAge(corsProperties.getMaxAge());
// 设置访问源请求方法
config.addAllowedMethod("*");
// 有效期 1800秒
config.setMaxAge(1800L);
// 添加映射路径,拦截一切请求 // 添加映射路径,拦截一切请求
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config); source.registerCorsConfiguration("/**", config);
@@ -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);
// });
// });
// }
//
//}
@@ -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;
}
@@ -13,6 +13,7 @@ import java.io.BufferedReader;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
/** /**
* 构建可重复读取输入流的请求包装器,缓存请求体以支持多次读取。 * 构建可重复读取输入流的请求包装器,缓存请求体以支持多次读取。
@@ -45,7 +46,7 @@ public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper {
*/ */
@Override @Override
public BufferedReader getReader() throws IOException { 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 @Override
public int available() throws IOException { public int available() throws IOException {
return body.length; return bais.available();
} }
@Override @Override
public boolean isFinished() { public boolean isFinished() {
return false; return bais.available() == 0;
} }
@Override @Override
public boolean isReady() { public boolean isReady() {
return false; return true;
} }
@Override @Override
@@ -1,6 +1,6 @@
package org.dromara.common.web.filter; 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.core.utils.StringUtils;
import org.dromara.common.web.config.properties.XssProperties; import org.dromara.common.web.config.properties.XssProperties;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
@@ -17,11 +17,14 @@ import java.util.List;
* *
* @author ruoyi * @author ruoyi
*/ */
@RequiredArgsConstructor
public class XssFilter implements Filter { public class XssFilter implements Filter {
/** /**
* 跳过 XSS 过滤的请求路径集合。 * 跳过 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 @Override
public void init(FilterConfig filterConfig) throws ServletException { public void init(FilterConfig filterConfig) throws ServletException {
XssProperties properties = SpringUtils.getBean(XssProperties.class); if (properties.getExcludeUrls() != null) {
excludes.addAll(properties.getExcludeUrls()); excludes.addAll(properties.getExcludeUrls());
}
} }
/** /**
@@ -213,7 +213,7 @@ public class GlobalExceptionHandler {
public R<Void> handleJsonParseException(JsonParseException e, HttpServletRequest request) { public R<Void> handleJsonParseException(JsonParseException e, HttpServletRequest request) {
String requestURI = request.getRequestURI(); String requestURI = request.getRequestURI();
log.error("请求地址'{}' 发生 JSON 解析异常: {}", requestURI, e.getMessage()); 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) @ExceptionHandler(HttpMessageNotReadableException.class)
public R<Void> handleHttpMessageNotReadableException(HttpMessageNotReadableException e, HttpServletRequest request) { public R<Void> handleHttpMessageNotReadableException(HttpMessageNotReadableException e, HttpServletRequest request) {
log.error("请求地址'{}', 参数解析失败: {}", request.getRequestURI(), e.getMessage()); log.error("请求地址'{}', 参数解析失败: {}", request.getRequestURI(), e.getMessage());
return R.fail(HttpStatus.HTTP_BAD_REQUEST, "请求参数格式错误" + e.getMostSpecificCause().getMessage()); return R.fail(HttpStatus.HTTP_BAD_REQUEST, "请求参数格式错误");
} }
/** /**
@@ -14,7 +14,6 @@ import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.web.filter.RepeatedlyRequestWrapper; import org.dromara.common.web.filter.RepeatedlyRequestWrapper;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import tools.jackson.databind.JsonNode; import tools.jackson.databind.JsonNode;
import tools.jackson.databind.json.JsonMapper; import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.node.ArrayNode; 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 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) { if (request instanceof RepeatedlyRequestWrapper) {
jsonParam = IoUtil.read(request.getReader()); jsonParam = IoUtil.read(request.getReader());
if (StringUtils.isNotBlank(jsonParam)) { if (StringUtils.isNotBlank(jsonParam)) {
JsonMapper jsonMapper = JsonUtils.getJsonMapper(); jsonParam = sanitizeJsonParam(jsonParam);
JsonNode rootNode = jsonMapper.readTree(jsonParam);
removeSensitiveFields(rootNode, SystemConstants.EXCLUDE_PROPERTIES);
jsonParam = rootNode.toString();
} }
} }
log.info("[PLUS]开始请求 => URL[{}],参数类型[json],参数:[{}]", url, jsonParam); log.info("[PLUS]开始请求 => URL[{}],参数类型[json],参数:[{}]", url, limit(jsonParam));
} else { } else {
Map<String, String[]> parameterMap = request.getParameterMap(); Map<String, String[]> parameterMap = request.getParameterMap();
if (MapUtil.isNotEmpty(parameterMap)) { if (MapUtil.isNotEmpty(parameterMap)) {
Map<String, String[]> map = new LinkedHashMap<>(parameterMap); Map<String, String[]> map = new LinkedHashMap<>(parameterMap);
MapUtil.removeAny(map, SystemConstants.EXCLUDE_PROPERTIES); MapUtil.removeAny(map, SystemConstants.EXCLUDE_PROPERTIES);
String parameters = JsonUtils.toJsonString(map); String parameters = JsonUtils.toJsonString(map);
log.info("[PLUS]开始请求 => URL[{}],参数类型[param],参数:[{}]", url, parameters); log.info("[PLUS]开始请求 => URL[{}],参数类型[param],参数:[{}]", url, limit(parameters));
} else { } else {
log.info("[PLUS]开始请求 => URL[{}],无参数", url); 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);
} }
/** /**