mirror of
https://github.com/dromara/RuoYi-Vue-Plus.git
synced 2026-06-12 08:46:11 +00:00
refactor(common-web): 优化 Web 基础配置与请求处理
- 修复可重复读取请求体包装器的 ServletInputStream 状态判断 - 将 XSS 过滤器改为构造器注入配置,移除静态 Spring Bean 获取 - 增加 CORS 配置属性,支持通过 web.cors.* 配置跨域规则 - 优化请求日志参数处理,避免 JSON 解析异常影响主请求并限制日志长度 - 收敛请求体解析异常响应,避免向前端暴露底层异常细节
This commit is contained in:
@@ -32,7 +32,7 @@
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<!-- web 容器使用 undertow 性能更强 -->
|
||||
<!-- web 容器使用 jetty -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-jetty</artifactId>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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, "请求参数格式错误");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user