init: 导入RuoYi‑Vue‑Plus 6.X完整代码
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>ruoyi-common-web</artifactId>
|
||||
|
||||
<description>
|
||||
ruoyi-common-web web服务
|
||||
</description>
|
||||
|
||||
<dependencies>
|
||||
<!-- 序列化模块 -->
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-json</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- SpringBoot Web容器 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<artifactId>spring-boot-starter-tomcat</artifactId>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<!-- web 容器使用 jetty -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-jetty</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 监控端点 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 图形验证码 -->
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-captcha</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Hutool 加密工具 -->
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-crypto</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package org.dromara.common.web.advice;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.common.json.enhance.JsonValueEnhancer;
|
||||
import org.jspecify.annotations.NonNull;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
|
||||
|
||||
/**
|
||||
* 响应体统一增强拦截器。
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
@RequiredArgsConstructor
|
||||
public class ResponseEnhancementAdvice implements ResponseBodyAdvice<Object> {
|
||||
|
||||
private final JsonValueEnhancer jsonValueEnhancer;
|
||||
|
||||
@Override
|
||||
public boolean supports(@NonNull MethodParameter returnType,
|
||||
@NonNull Class<? extends HttpMessageConverter<?>> converterType) {
|
||||
return jsonValueEnhancer.supports(converterType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object beforeBodyWrite(Object body,
|
||||
@NonNull MethodParameter returnType,
|
||||
@NonNull MediaType selectedContentType,
|
||||
@NonNull Class<? extends HttpMessageConverter<?>> selectedConverterType,
|
||||
@NonNull ServerHttpRequest request,
|
||||
@NonNull ServerHttpResponse response) {
|
||||
if (!selectedContentType.isCompatibleWith(MediaType.APPLICATION_JSON)) {
|
||||
return body;
|
||||
}
|
||||
return jsonValueEnhancer.enhance(body);
|
||||
}
|
||||
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package org.dromara.common.web.config;
|
||||
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import org.dromara.common.web.config.properties.XssProperties;
|
||||
import org.dromara.common.web.filter.RepeatableFilter;
|
||||
import org.dromara.common.web.filter.XssFilter;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.web.servlet.FilterRegistration;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
/**
|
||||
* Filter配置
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@EnableConfigurationProperties(XssProperties.class)
|
||||
public class FilterConfig {
|
||||
|
||||
/**
|
||||
* 注册 XSS 过滤器。
|
||||
*
|
||||
* @param xssProperties XSS 配置
|
||||
* @return XSS 请求过滤器实例
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnProperty(value = "xss.enabled", havingValue = "true")
|
||||
@FilterRegistration(
|
||||
name = "xssFilter",
|
||||
urlPatterns = "/*",
|
||||
order = FilterRegistrationBean.HIGHEST_PRECEDENCE + 1,
|
||||
dispatcherTypes = DispatcherType.REQUEST
|
||||
)
|
||||
public XssFilter xssFilter(XssProperties xssProperties) {
|
||||
return new XssFilter(xssProperties);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册可重复读取请求体过滤器。
|
||||
*
|
||||
* @return 请求包装过滤器实例
|
||||
*/
|
||||
@Bean
|
||||
@FilterRegistration(name = "repeatableFilter", urlPatterns = "/*")
|
||||
public RepeatableFilter repeatableFilter() {
|
||||
return new RepeatableFilter();
|
||||
}
|
||||
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package org.dromara.common.web.config;
|
||||
|
||||
import org.dromara.common.web.core.I18nLocaleResolver;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.web.servlet.LocaleResolver;
|
||||
|
||||
/**
|
||||
* 国际化配置
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@AutoConfiguration(before = WebMvcAutoConfiguration.class)
|
||||
public class I18nConfig {
|
||||
|
||||
/**
|
||||
* 注册自定义国际化区域解析器。
|
||||
*
|
||||
* @return Locale 解析器实例
|
||||
*/
|
||||
@Bean
|
||||
public LocaleResolver localeResolver() {
|
||||
return new I18nLocaleResolver();
|
||||
}
|
||||
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package org.dromara.common.web.config;
|
||||
|
||||
import cn.hutool.core.date.DateTime;
|
||||
import org.dromara.common.core.utils.DateUtils;
|
||||
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.WebMvcConfigurer;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 通用配置
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@EnableConfigurationProperties(CorsProperties.class)
|
||||
public class ResourcesConfig implements WebMvcConfigurer {
|
||||
|
||||
/**
|
||||
* 注册全局拦截器。
|
||||
*
|
||||
* @param registry 拦截器注册表
|
||||
*/
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// 全局访问性能拦截
|
||||
registry.addInterceptor(new PlusWebInvokeTimeInterceptor());
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册全局格式转换器。
|
||||
*
|
||||
* @param registry 格式化器注册表
|
||||
*/
|
||||
@Override
|
||||
public void addFormatters(FormatterRegistry registry) {
|
||||
// 全局日期格式转换配置
|
||||
registry.addConverter(String.class, Date.class, source -> {
|
||||
DateTime parse = DateUtils.parse(source);
|
||||
if (ObjectUtils.isNull(parse)) {
|
||||
return null;
|
||||
}
|
||||
return parse.toJdkDate();
|
||||
});
|
||||
registry.addConverter(String.class, LocalDateTime.class, source -> {
|
||||
DateTime parse = DateUtils.parse(source);
|
||||
if (ObjectUtils.isNull(parse)) {
|
||||
return null;
|
||||
}
|
||||
return parse.toLocalDateTime();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 跨域配置
|
||||
*
|
||||
* @return 全局 Cors 过滤器
|
||||
*/
|
||||
@Bean
|
||||
public CorsFilter corsFilter(CorsProperties corsProperties) {
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
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);
|
||||
// 返回新的CorsFilter
|
||||
return new CorsFilter(source);
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局异常处理器
|
||||
*
|
||||
* @return 全局异常处理器实例
|
||||
*/
|
||||
@Bean
|
||||
public GlobalExceptionHandler globalExceptionHandler() {
|
||||
return new GlobalExceptionHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册响应增强处理器。
|
||||
*
|
||||
* @param jsonValueEnhancer JSON 字段增强器
|
||||
* @return 响应增强处理器
|
||||
*/
|
||||
@Bean
|
||||
public ResponseEnhancementAdvice responseEnhancementAdvice(JsonValueEnhancer jsonValueEnhancer) {
|
||||
return new ResponseEnhancementAdvice(jsonValueEnhancer);
|
||||
}
|
||||
|
||||
}
|
||||
+41
@@ -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;
|
||||
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* XSS 过滤配置属性,用于控制过滤器开关及排除路径。
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Data
|
||||
@ConfigurationProperties(prefix = "xss")
|
||||
public class XssProperties {
|
||||
|
||||
/**
|
||||
* XSS 过滤总开关。
|
||||
*/
|
||||
private Boolean enabled;
|
||||
|
||||
/**
|
||||
* 跳过 XSS 过滤的请求路径集合。
|
||||
*/
|
||||
private List<String> excludeUrls = new ArrayList<>();
|
||||
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package org.dromara.common.web.core;
|
||||
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
|
||||
/**
|
||||
* web层通用数据处理
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
public class BaseController {
|
||||
|
||||
/**
|
||||
* 响应返回结果
|
||||
*
|
||||
* @param rows 影响行数
|
||||
* @return 操作结果
|
||||
*/
|
||||
protected R<Void> toAjax(int rows) {
|
||||
return rows > 0 ? R.ok() : R.fail();
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应返回结果
|
||||
*
|
||||
* @param result 结果
|
||||
* @return 操作结果
|
||||
*/
|
||||
protected R<Void> toAjax(boolean result) {
|
||||
return result ? R.ok() : R.fail();
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面跳转
|
||||
*
|
||||
* @param url 目标跳转地址
|
||||
* @return Spring MVC 重定向路径表达式
|
||||
*/
|
||||
public String redirect(String url) {
|
||||
return StringUtils.format("redirect:{}", url);
|
||||
}
|
||||
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package org.dromara.common.web.core;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.web.servlet.LocaleResolver;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* 基于请求头解析国际化区域信息的语言解析器。
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
public class I18nLocaleResolver implements LocaleResolver {
|
||||
|
||||
/**
|
||||
* 从请求头 {@code content-language} 中解析本次请求的区域信息,缺省时回退到系统默认区域。
|
||||
*
|
||||
* @param httpServletRequest 当前请求
|
||||
* @return 当前请求对应的区域设置
|
||||
*/
|
||||
@Override
|
||||
public Locale resolveLocale(HttpServletRequest httpServletRequest) {
|
||||
String language = httpServletRequest.getHeader("content-language");
|
||||
Locale locale = Locale.getDefault();
|
||||
if (language != null && !language.isEmpty()) {
|
||||
locale = Locale.forLanguageTag(language.replace('_', '-'));
|
||||
}
|
||||
return locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前项目不在服务端主动切换区域信息,因此保留空实现。
|
||||
*
|
||||
* @param httpServletRequest 当前请求
|
||||
* @param httpServletResponse 当前响应
|
||||
* @param locale 目标区域
|
||||
*/
|
||||
@Override
|
||||
public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) {
|
||||
|
||||
}
|
||||
}
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
package org.dromara.common.web.core;
|
||||
|
||||
import cn.hutool.captcha.AbstractCaptcha;
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.captcha.generator.RandomGenerator;
|
||||
import cn.hutool.core.img.GraphicsUtil;
|
||||
import cn.hutool.core.img.ImgUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.Serial;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
* 带干扰线、波浪和圆形干扰元素的验证码实现,用于增强验证码识别难度。
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
public class WaveAndCircleCaptcha extends AbstractCaptcha {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 构造默认长度为 4 的验证码。
|
||||
*
|
||||
* @param width 图片宽度
|
||||
* @param height 图片高度
|
||||
*/
|
||||
public WaveAndCircleCaptcha(int width, int height) {
|
||||
this(width, height, 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造指定验证码长度的验证码对象。
|
||||
*
|
||||
* @param width 图片宽度
|
||||
* @param height 图片高度
|
||||
* @param codeCount 验证码字符数
|
||||
*/
|
||||
public WaveAndCircleCaptcha(int width, int height, int codeCount) {
|
||||
this(width, height, codeCount, 6);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造指定字符数与干扰数的验证码对象。
|
||||
*
|
||||
* @param width 图片宽度
|
||||
* @param height 图片高度
|
||||
* @param codeCount 验证码字符数
|
||||
* @param interfereCount 干扰元素数量
|
||||
*/
|
||||
public WaveAndCircleCaptcha(int width, int height, int codeCount, int interfereCount) {
|
||||
this(width, height, new RandomGenerator(codeCount), interfereCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用指定验证码生成器构造验证码对象。
|
||||
*
|
||||
* @param width 图片宽度
|
||||
* @param height 图片高度
|
||||
* @param generator 验证码生成器
|
||||
* @param interfereCount 干扰元素数量
|
||||
*/
|
||||
public WaveAndCircleCaptcha(int width, int height, CodeGenerator generator, int interfereCount) {
|
||||
super(width, height, generator, interfereCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造带字体缩放比例的验证码对象。
|
||||
*
|
||||
* @param width 图片宽度
|
||||
* @param height 图片高度
|
||||
* @param codeCount 验证码字符数
|
||||
* @param interfereCount 干扰元素数量
|
||||
* @param size 字体相对尺寸
|
||||
*/
|
||||
public WaveAndCircleCaptcha(int width, int height, int codeCount, int interfereCount, float size) {
|
||||
super(width, height, new RandomGenerator(codeCount), interfereCount, size);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成验证码图片并绘制文字、扭曲效果及干扰图形。
|
||||
*
|
||||
* @param code 验证码文本
|
||||
* @return 生成后的验证码图片
|
||||
*/
|
||||
@Override
|
||||
public Image createImage(String code) {
|
||||
final BufferedImage image = new BufferedImage(
|
||||
width,
|
||||
height,
|
||||
(null == this.background) ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_INT_RGB
|
||||
);
|
||||
final Graphics2D g = ImgUtil.createGraphics(image, this.background);
|
||||
|
||||
try {
|
||||
drawString(g, code);
|
||||
// 扭曲
|
||||
shear(g, this.width, this.height, ObjectUtil.defaultIfNull(this.background, Color.WHITE));
|
||||
drawInterfere(g);
|
||||
} finally {
|
||||
g.dispose();
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制验证码文本并开启文字抗锯齿。
|
||||
*
|
||||
* @param g 图形上下文
|
||||
* @param code 验证码文本
|
||||
*/
|
||||
private void drawString(Graphics2D g, String code) {
|
||||
// 设置抗锯齿(让字体渲染更清晰)
|
||||
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||
|
||||
if (this.textAlpha != null) {
|
||||
g.setComposite(this.textAlpha);
|
||||
}
|
||||
|
||||
GraphicsUtil.drawStringColourful(g, code, this.font, this.width, this.height);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制圆形与波浪线干扰元素。
|
||||
*
|
||||
* @param g 图形上下文
|
||||
*/
|
||||
protected void drawInterfere(Graphics2D g) {
|
||||
ThreadLocalRandom random = RandomUtil.getRandom();
|
||||
int circleCount = Math.max(0, this.interfereCount - 1);
|
||||
|
||||
// 圈圈
|
||||
for (int i = 0; i < circleCount; i++) {
|
||||
g.setColor(ImgUtil.randomColor(random));
|
||||
int x = random.nextInt(width);
|
||||
int y = random.nextInt(height);
|
||||
int w = random.nextInt(height >> 1);
|
||||
int h = random.nextInt(height >> 1);
|
||||
g.drawOval(x, y, w, h);
|
||||
}
|
||||
|
||||
// 仅 1 条平滑波浪线
|
||||
if (this.interfereCount >= 1) {
|
||||
g.setColor(getRandomColor(120, 230, random));
|
||||
drawSmoothWave(g, random);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制平滑波浪线干扰轨迹。
|
||||
*
|
||||
* @param g 图形上下文
|
||||
* @param random 随机数生成器
|
||||
*/
|
||||
private void drawSmoothWave(Graphics2D g, ThreadLocalRandom random) {
|
||||
int amplitude = random.nextInt(8) + 5; // 波动幅度
|
||||
int wavelength = random.nextInt(40) + 30; // 波长
|
||||
double phase = random.nextDouble() * Math.PI * 2;
|
||||
|
||||
// ✅ 关键:限制 baseY 在中间区域
|
||||
int centerY = height / 2;
|
||||
int verticalJitter = Math.max(5, height / 6); // 至少偏移5像素
|
||||
int baseY = centerY - verticalJitter + random.nextInt(verticalJitter * 2);
|
||||
|
||||
g.setStroke(new BasicStroke(2.5f)); // 线宽
|
||||
|
||||
int[] xPoints = new int[width];
|
||||
int[] yPoints = new int[width];
|
||||
for (int x = 0; x < width; x++) {
|
||||
int y = baseY + (int) (amplitude * Math.sin((double) x / wavelength * 2 * Math.PI + phase));
|
||||
// 限制 y 不要超出图像边界(可选)
|
||||
y = Math.max(amplitude, Math.min(y, height - amplitude));
|
||||
xPoints[x] = x;
|
||||
yPoints[x] = y;
|
||||
}
|
||||
g.drawPolyline(xPoints, yPoints, width);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成指定 RGB 范围内的随机颜色。
|
||||
*
|
||||
* @param min 最小颜色值
|
||||
* @param max 最大颜色值
|
||||
* @param random 随机数生成器
|
||||
* @return 随机颜色
|
||||
*/
|
||||
private Color getRandomColor(int min, int max, ThreadLocalRandom random) {
|
||||
int range = max - min;
|
||||
return new Color(
|
||||
min + random.nextInt(range),
|
||||
min + random.nextInt(range),
|
||||
min + random.nextInt(range)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 扭曲
|
||||
*
|
||||
* @param g {@link Graphics}
|
||||
* @param w1 w1
|
||||
* @param h1 h1
|
||||
* @param color 颜色
|
||||
*/
|
||||
private void shear(Graphics g, int w1, int h1, Color color) {
|
||||
shearX(g, w1, h1, color);
|
||||
shearY(g, w1, h1, color);
|
||||
}
|
||||
|
||||
/**
|
||||
* X坐标扭曲
|
||||
*
|
||||
* @param g {@link Graphics}
|
||||
* @param w1 宽
|
||||
* @param h1 高
|
||||
* @param color 颜色
|
||||
*/
|
||||
private void shearX(Graphics g, int w1, int h1, Color color) {
|
||||
|
||||
int period = RandomUtil.randomInt(this.width);
|
||||
|
||||
int frames = 1;
|
||||
int phase = RandomUtil.randomInt(2);
|
||||
|
||||
for (int i = 0; i < h1; i++) {
|
||||
double d = (double) (period >> 1) * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames);
|
||||
g.copyArea(0, i, w1, 1, (int) d, 0);
|
||||
g.setColor(color);
|
||||
g.drawLine((int) d, i, 0, i);
|
||||
g.drawLine((int) d + w1, i, w1, i);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Y坐标扭曲
|
||||
*
|
||||
* @param g {@link Graphics}
|
||||
* @param w1 宽
|
||||
* @param h1 高
|
||||
* @param color 颜色
|
||||
*/
|
||||
private void shearY(Graphics g, int w1, int h1, Color color) {
|
||||
|
||||
int period = RandomUtil.randomInt(this.height >> 1);
|
||||
|
||||
int frames = 20;
|
||||
int phase = 7;
|
||||
for (int i = 0; i < w1; i++) {
|
||||
double d = (double) (period >> 1) * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames);
|
||||
g.copyArea(i, 0, 1, h1, 0, (int) d);
|
||||
g.setColor(color);
|
||||
// 擦除原位置的痕迹
|
||||
g.drawLine(i, (int) d, i, 0);
|
||||
g.drawLine(i, (int) d + h1, i, h1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package org.dromara.common.web.filter;
|
||||
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 可重复读取请求体的过滤器,仅对 JSON 请求包装可重复消费的请求对象。
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class RepeatableFilter implements Filter {
|
||||
|
||||
/**
|
||||
* 过滤器初始化入口,当前无额外初始化逻辑。
|
||||
*
|
||||
* @param filterConfig 过滤器配置
|
||||
* @throws ServletException 过滤器初始化异常
|
||||
*/
|
||||
@Override
|
||||
public void init(FilterConfig filterConfig) throws ServletException {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 为 JSON 请求创建可重复读取的包装器,便于日志、验签等场景多次读取请求体。
|
||||
*
|
||||
* @param request 原始请求
|
||||
* @param response 当前响应
|
||||
* @param chain 过滤器链
|
||||
* @throws IOException IO 异常
|
||||
* @throws ServletException Servlet 异常
|
||||
*/
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
ServletRequest requestWrapper = null;
|
||||
if (request instanceof HttpServletRequest
|
||||
&& StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)) {
|
||||
requestWrapper = new RepeatedlyRequestWrapper((HttpServletRequest) request, response);
|
||||
}
|
||||
if (null == requestWrapper) {
|
||||
chain.doFilter(request, response);
|
||||
} else {
|
||||
chain.doFilter(requestWrapper, response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤器销毁入口,当前无额外资源需要释放。
|
||||
*/
|
||||
@Override
|
||||
public void destroy() {
|
||||
|
||||
}
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
package org.dromara.common.web.filter;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import jakarta.servlet.ReadListener;
|
||||
import jakarta.servlet.ServletInputStream;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* 构建可重复读取输入流的请求包装器,缓存请求体以支持多次读取。
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper {
|
||||
/**
|
||||
* 请求体字节数据。
|
||||
*/
|
||||
private final byte[] body;
|
||||
|
||||
/**
|
||||
* 读取原始请求体并缓存到内存,统一设置请求与响应编码。
|
||||
*
|
||||
* @param request 原始请求
|
||||
* @param response 当前响应
|
||||
* @throws IOException 读取请求体异常
|
||||
*/
|
||||
public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException {
|
||||
super(request);
|
||||
request.setCharacterEncoding(Constants.UTF8);
|
||||
response.setCharacterEncoding(Constants.UTF8);
|
||||
|
||||
body = IoUtil.readBytes(request.getInputStream(), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于缓存的请求体构造字符读取器。
|
||||
*
|
||||
* @return 可重复读取的字符流
|
||||
* @throws IOException IO 异常
|
||||
*/
|
||||
@Override
|
||||
public BufferedReader getReader() throws IOException {
|
||||
return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回基于缓存请求体重新生成的输入流。
|
||||
*
|
||||
* @return 可重复读取的输入流
|
||||
* @throws IOException IO 异常
|
||||
*/
|
||||
@Override
|
||||
public ServletInputStream getInputStream() throws IOException {
|
||||
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
|
||||
return new ServletInputStream() {
|
||||
/**
|
||||
* 读取缓存请求体的下一个字节。
|
||||
*
|
||||
* @return 下一个字节
|
||||
* @throws IOException IO 异常
|
||||
*/
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
return bais.read();
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回缓存请求体剩余可读字节数。
|
||||
*
|
||||
* @return 剩余字节数
|
||||
* @throws IOException IO 异常
|
||||
*/
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
return bais.available();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断缓存请求体是否已读取完毕。
|
||||
*
|
||||
* @return 是否读取完毕
|
||||
*/
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return bais.available() == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断输入流是否可读。
|
||||
*
|
||||
* @return 固定为 true
|
||||
*/
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置异步读取监听器。
|
||||
*
|
||||
* @param readListener 读取监听器
|
||||
*/
|
||||
@Override
|
||||
public void setReadListener(ReadListener readListener) {
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package org.dromara.common.web.filter;
|
||||
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.web.config.properties.XssProperties;
|
||||
import org.springframework.http.HttpMethod;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 防止 XSS 攻击的过滤器,对非排除请求执行参数与请求体清洗。
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class XssFilter implements Filter {
|
||||
/**
|
||||
* 跳过 XSS 过滤的请求路径集合。
|
||||
*/
|
||||
private final List<String> excludes = new ArrayList<>();
|
||||
|
||||
private final XssProperties properties;
|
||||
|
||||
/**
|
||||
* 初始化过滤器并加载配置中的排除路径。
|
||||
*
|
||||
* @param filterConfig 过滤器配置
|
||||
* @throws ServletException 过滤器初始化异常
|
||||
*/
|
||||
@Override
|
||||
public void init(FilterConfig filterConfig) throws ServletException {
|
||||
if (properties.getExcludeUrls() != null) {
|
||||
excludes.addAll(properties.getExcludeUrls());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对请求执行 XSS 包装处理,命中排除规则时直接放行。
|
||||
*
|
||||
* @param request 原始请求
|
||||
* @param response 当前响应
|
||||
* @param chain 过滤器链
|
||||
* @throws IOException IO 异常
|
||||
* @throws ServletException Servlet 异常
|
||||
*/
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
HttpServletRequest req = (HttpServletRequest) request;
|
||||
HttpServletResponse resp = (HttpServletResponse) response;
|
||||
if (handleExcludeURL(req, resp)) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest) request);
|
||||
chain.doFilter(xssRequest, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前请求是否需要跳过 XSS 过滤。
|
||||
*
|
||||
* @param request 当前请求
|
||||
* @param response 当前响应
|
||||
* @return true 表示跳过过滤
|
||||
*/
|
||||
private boolean handleExcludeURL(HttpServletRequest request, HttpServletResponse response) {
|
||||
String url = request.getServletPath();
|
||||
String method = request.getMethod();
|
||||
// GET DELETE 不过滤
|
||||
if (method == null || HttpMethod.GET.matches(method) || HttpMethod.DELETE.matches(method)) {
|
||||
return true;
|
||||
}
|
||||
return StringUtils.matches(url, excludes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤器销毁入口,当前无额外资源需要释放。
|
||||
*/
|
||||
@Override
|
||||
public void destroy() {
|
||||
|
||||
}
|
||||
}
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
package org.dromara.common.web.filter;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HtmlUtil;
|
||||
import jakarta.servlet.ReadListener;
|
||||
import jakarta.servlet.ServletInputStream;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* XSS 请求包装器,统一清洗参数与 JSON 请求体中的 HTML 标签内容。
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
/**
|
||||
* 使用原始请求构造 XSS 包装器。
|
||||
*
|
||||
* @param request 原始请求
|
||||
*/
|
||||
public XssHttpServletRequestWrapper(HttpServletRequest request) {
|
||||
super(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取并清洗单个请求参数。
|
||||
*
|
||||
* @param name 参数名
|
||||
* @return 清洗后的参数值
|
||||
*/
|
||||
@Override
|
||||
public String getParameter(String name) {
|
||||
String value = super.getParameter(name);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return HtmlUtil.cleanHtmlTag(value).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取并清洗整组请求参数。
|
||||
*
|
||||
* @return 清洗后的参数映射
|
||||
*/
|
||||
@Override
|
||||
public Map<String, String[]> getParameterMap() {
|
||||
Map<String, String[]> valueMap = super.getParameterMap();
|
||||
if (MapUtil.isEmpty(valueMap)) {
|
||||
return valueMap;
|
||||
}
|
||||
// 避免某些容器不允许改参数的情况 copy一份重新改
|
||||
Map<String, String[]> map = new HashMap<>(valueMap.size());
|
||||
map.putAll(valueMap);
|
||||
for (Map.Entry<String, String[]> entry : map.entrySet()) {
|
||||
String[] values = entry.getValue();
|
||||
if (values != null) {
|
||||
int length = values.length;
|
||||
String[] escapseValues = new String[length];
|
||||
for (int i = 0; i < length; i++) {
|
||||
// 防xss攻击和过滤前后空格
|
||||
escapseValues[i] = HtmlUtil.cleanHtmlTag(values[i]).trim();
|
||||
}
|
||||
map.put(entry.getKey(), escapseValues);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取并清洗指定参数的多值数组。
|
||||
*
|
||||
* @param name 参数名
|
||||
* @return 清洗后的参数值数组
|
||||
*/
|
||||
@Override
|
||||
public String[] getParameterValues(String name) {
|
||||
String[] values = super.getParameterValues(name);
|
||||
if (ArrayUtil.isEmpty(values)) {
|
||||
return values;
|
||||
}
|
||||
int length = values.length;
|
||||
String[] escapseValues = new String[length];
|
||||
for (int i = 0; i < length; i++) {
|
||||
// 防xss攻击和过滤前后空格
|
||||
escapseValues[i] = HtmlUtil.cleanHtmlTag(values[i]).trim();
|
||||
}
|
||||
return escapseValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取输入流并在 JSON 场景下对请求体执行清洗。
|
||||
*
|
||||
* @return 清洗后的输入流
|
||||
* @throws IOException 读取请求体异常
|
||||
*/
|
||||
@Override
|
||||
public ServletInputStream getInputStream() throws IOException {
|
||||
// 非json类型,直接返回
|
||||
if (!isJsonRequest()) {
|
||||
return super.getInputStream();
|
||||
}
|
||||
|
||||
// 为空,直接返回
|
||||
String json = StrUtil.str(IoUtil.readBytes(super.getInputStream(), false), StandardCharsets.UTF_8);
|
||||
if (StringUtils.isEmpty(json)) {
|
||||
return super.getInputStream();
|
||||
}
|
||||
|
||||
// xss过滤
|
||||
json = HtmlUtil.cleanHtmlTag(json).trim();
|
||||
byte[] jsonBytes = json.getBytes(StandardCharsets.UTF_8);
|
||||
final ByteArrayInputStream bis = IoUtil.toStream(jsonBytes);
|
||||
return new ServletInputStream() {
|
||||
/**
|
||||
* 判断清洗后的 JSON 流是否已读取完毕。
|
||||
*
|
||||
* @return 是否读取完毕
|
||||
*/
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断清洗后的 JSON 流是否可读。
|
||||
*
|
||||
* @return 固定为 true
|
||||
*/
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回清洗后的 JSON 字节数。
|
||||
*
|
||||
* @return JSON 字节数
|
||||
* @throws IOException IO 异常
|
||||
*/
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
return jsonBytes.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置异步读取监听器。
|
||||
*
|
||||
* @param readListener 读取监听器
|
||||
*/
|
||||
@Override
|
||||
public void setReadListener(ReadListener readListener) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取清洗后的 JSON 流下一个字节。
|
||||
*
|
||||
* @return 下一个字节
|
||||
* @throws IOException IO 异常
|
||||
*/
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
return bis.read();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前请求是否为 JSON 请求。
|
||||
*
|
||||
* @return true 表示 JSON 请求
|
||||
*/
|
||||
public boolean isJsonRequest() {
|
||||
String header = super.getHeader(HttpHeaders.CONTENT_TYPE);
|
||||
return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE);
|
||||
}
|
||||
}
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
package org.dromara.common.web.handler;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import cn.hutool.http.HttpStatus;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.exception.SseException;
|
||||
import org.dromara.common.core.exception.base.BaseException;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
import org.dromara.common.core.utils.StreamUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.springframework.boot.json.JsonParseException;
|
||||
import org.springframework.context.MessageSourceResolvable;
|
||||
import org.springframework.context.support.DefaultMessageSourceResolvable;
|
||||
import org.springframework.expression.ExpressionException;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.MissingPathVariableException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
|
||||
import org.springframework.web.method.annotation.HandlerMethodValidationException;
|
||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 全局异常处理器
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
/**
|
||||
* 请求方式不支持
|
||||
*/
|
||||
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
|
||||
public R<Void> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e,
|
||||
HttpServletRequest request) {
|
||||
String requestURI = request.getRequestURI();
|
||||
log.error("请求地址'{}',不支持'{}'请求", requestURI, e.getMethod());
|
||||
return R.fail(HttpStatus.HTTP_BAD_METHOD, e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 业务异常
|
||||
*/
|
||||
@ExceptionHandler(ServiceException.class)
|
||||
public R<Void> handleServiceException(ServiceException e, HttpServletRequest request) {
|
||||
log.error(e.getMessage());
|
||||
Integer code = e.getCode();
|
||||
return ObjectUtil.isNotNull(code) ? R.fail(code, e.getMessage()) : R.fail(e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证失败
|
||||
*/
|
||||
@ResponseStatus(org.springframework.http.HttpStatus.UNAUTHORIZED)
|
||||
@ExceptionHandler(SseException.class)
|
||||
public String handleNotLoginException(SseException e, HttpServletRequest request) {
|
||||
String requestURI = request.getRequestURI();
|
||||
log.debug("请求地址'{}',认证失败'{}',无法访问系统资源", requestURI, e.getMessage());
|
||||
return JsonUtils.toJsonString(R.fail(HttpStatus.HTTP_UNAUTHORIZED, "认证失败,无法访问系统资源"));
|
||||
}
|
||||
|
||||
/**
|
||||
* servlet异常
|
||||
*/
|
||||
@ExceptionHandler(ServletException.class)
|
||||
public R<Void> handleServletException(ServletException e, HttpServletRequest request) {
|
||||
String requestURI = request.getRequestURI();
|
||||
log.error("请求地址'{}',发生未知异常.", requestURI, e);
|
||||
return R.fail("发生未知异常,请联系管理员");
|
||||
}
|
||||
|
||||
/**
|
||||
* 业务异常
|
||||
*/
|
||||
@ExceptionHandler(BaseException.class)
|
||||
public R<Void> handleBaseException(BaseException e, HttpServletRequest request) {
|
||||
log.error(e.getMessage());
|
||||
return R.fail(e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求路径中缺少必需的路径变量
|
||||
*/
|
||||
@ExceptionHandler(MissingPathVariableException.class)
|
||||
public R<Void> handleMissingPathVariableException(MissingPathVariableException e, HttpServletRequest request) {
|
||||
String requestURI = request.getRequestURI();
|
||||
log.error("请求路径中缺少必需的路径变量'{}',发生系统异常.", requestURI);
|
||||
return R.fail(String.format("请求路径中缺少必需的路径变量[%s]", e.getVariableName()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求参数类型不匹配
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
|
||||
public R<Void> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) {
|
||||
String requestURI = request.getRequestURI();
|
||||
log.error("请求参数类型不匹配'{}',发生系统异常.", requestURI);
|
||||
return R.fail(String.format("请求参数类型不匹配,参数[%s]要求类型为:'%s',但输入值为:'%s'", e.getName(), e.getRequiredType().getName(), e.getValue()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 找不到路由
|
||||
*/
|
||||
@ExceptionHandler(NoHandlerFoundException.class)
|
||||
public R<Void> handleNoHandlerFoundException(NoHandlerFoundException e, HttpServletRequest request) {
|
||||
String requestURI = request.getRequestURI();
|
||||
log.error("请求地址'{}'不存在.", requestURI);
|
||||
return R.fail(HttpStatus.HTTP_NOT_FOUND, "请求地址不存在");
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截未知的运行时异常
|
||||
*/
|
||||
@ResponseStatus(org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
@ExceptionHandler(IOException.class)
|
||||
public void handleIoException(IOException e, HttpServletRequest request) {
|
||||
String requestURI = request.getRequestURI();
|
||||
String path = SpringUtils.getProperty("message.path");
|
||||
if (requestURI.contains(path)) {
|
||||
// sse 经常性连接中断 例如关闭浏览器 直接屏蔽
|
||||
return;
|
||||
}
|
||||
log.error("请求地址'{}',连接中断", requestURI, e);
|
||||
}
|
||||
|
||||
/**
|
||||
* sse 连接超时异常 不需要处理
|
||||
*/
|
||||
@ExceptionHandler(AsyncRequestTimeoutException.class)
|
||||
public void handleRuntimeException(AsyncRequestTimeoutException e) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截未知的运行时异常
|
||||
*/
|
||||
@ExceptionHandler(RuntimeException.class)
|
||||
public R<Void> handleRuntimeException(RuntimeException e, HttpServletRequest request) {
|
||||
String requestURI = request.getRequestURI();
|
||||
String errorId = RandomUtil.randomNumbers(8);
|
||||
log.error("请求地址'{}',发生未知异常, 错误编号: {}", requestURI, errorId, e);
|
||||
return R.fail("发生未知异常,请联系管理员 [错误编号: " + errorId + "]");
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统异常
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
public R<Void> handleException(Exception e, HttpServletRequest request) {
|
||||
String requestURI = request.getRequestURI();
|
||||
String errorId = RandomUtil.randomNumbers(8);
|
||||
log.error("请求地址'{}',发生系统异常, 错误编号: {}", requestURI, errorId, e);
|
||||
return R.fail("发生系统异常,请联系管理员 [错误编号: " + errorId + "]");
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义验证异常
|
||||
*/
|
||||
@ExceptionHandler(BindException.class)
|
||||
public R<Void> handleBindException(BindException e) {
|
||||
log.error(e.getMessage());
|
||||
String message = StreamUtils.join(e.getAllErrors(), DefaultMessageSourceResolvable::getDefaultMessage, ", ");
|
||||
return R.fail(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义验证异常
|
||||
*/
|
||||
@ExceptionHandler(ConstraintViolationException.class)
|
||||
public R<Void> constraintViolationException(ConstraintViolationException e) {
|
||||
log.error(e.getMessage());
|
||||
String message = StreamUtils.join(e.getConstraintViolations(), ConstraintViolation::getMessage, ", ");
|
||||
return R.fail(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义验证异常
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public R<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
|
||||
log.error(e.getMessage());
|
||||
String message = StreamUtils.join(e.getBindingResult().getAllErrors(), DefaultMessageSourceResolvable::getDefaultMessage, ", ");
|
||||
return R.fail(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 方法参数校验异常 用于处理 @Validated 注解
|
||||
*/
|
||||
@ExceptionHandler(HandlerMethodValidationException.class)
|
||||
public R<Void> handlerMethodValidationException(HandlerMethodValidationException e) {
|
||||
log.error(e.getMessage());
|
||||
String message = StreamUtils.join(e.getAllErrors(), MessageSourceResolvable::getDefaultMessage, ", ");
|
||||
return R.fail(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 解析异常(Jackson 在处理 JSON 格式出错时抛出)
|
||||
* 可能是请求体格式非法,也可能是服务端反序列化失败
|
||||
*/
|
||||
@ExceptionHandler(JsonParseException.class)
|
||||
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, "请求数据格式错误");
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求体读取异常(通常是请求参数格式非法、字段类型不匹配等)
|
||||
*/
|
||||
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||
public R<Void> handleHttpMessageNotReadableException(HttpMessageNotReadableException e, HttpServletRequest request) {
|
||||
log.error("请求地址'{}', 参数解析失败: {}", request.getRequestURI(), e.getMessage());
|
||||
return R.fail(HttpStatus.HTTP_BAD_REQUEST, "请求参数格式错误");
|
||||
}
|
||||
|
||||
/**
|
||||
* SpEL 表达式相关异常
|
||||
*/
|
||||
@ExceptionHandler(ExpressionException.class)
|
||||
public R<Void> handleSpelException(ExpressionException e, HttpServletRequest request) {
|
||||
log.error("请求地址'{}',SpEL解析异常: {}", request.getRequestURI(), e.getMessage());
|
||||
return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, "SpEL解析失败:" + e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
package org.dromara.common.web.interceptor;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.time.StopWatch;
|
||||
import org.dromara.common.core.constant.SystemConstants;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
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 tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Web 调用时间统计拦截器,同时记录请求参数并对敏感字段做脱敏处理。
|
||||
*
|
||||
* @author Lion Li
|
||||
* @since 3.3.0
|
||||
*/
|
||||
@Slf4j
|
||||
public class PlusWebInvokeTimeInterceptor implements HandlerInterceptor {
|
||||
|
||||
private final static ThreadLocal<StopWatch> KEY_CACHE = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 请求参数日志最大长度。
|
||||
*/
|
||||
private static final int MAX_PARAM_LOG_LENGTH = 4000;
|
||||
|
||||
/**
|
||||
* 请求进入控制器前记录入参并启动耗时统计。
|
||||
*
|
||||
* @param request 当前请求
|
||||
* @param response 当前响应
|
||||
* @param handler 目标处理器
|
||||
* @return 始终返回 true,继续后续处理流程
|
||||
* @throws Exception 读取请求体或解析 JSON 失败时抛出
|
||||
*/
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
String url = request.getMethod() + " " + request.getRequestURI();
|
||||
// 打印请求参数
|
||||
if (isJsonRequest(request)) {
|
||||
String jsonParam = "";
|
||||
if (request instanceof RepeatedlyRequestWrapper) {
|
||||
jsonParam = IoUtil.read(request.getReader());
|
||||
if (StringUtils.isNotBlank(jsonParam)) {
|
||||
jsonParam = sanitizeJsonParam(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, limit(parameters));
|
||||
} else {
|
||||
log.info("[PLUS]开始请求 => URL[{}],无参数", url);
|
||||
}
|
||||
}
|
||||
|
||||
StopWatch stopWatch = new StopWatch();
|
||||
KEY_CACHE.set(stopWatch);
|
||||
stopWatch.start();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清洗 JSON 请求参数日志,解析失败时不影响主请求。
|
||||
*
|
||||
* @param jsonParam 原始 JSON 字符串
|
||||
* @return 清洗后的参数日志
|
||||
*/
|
||||
private String sanitizeJsonParam(String jsonParam) {
|
||||
try {
|
||||
JsonMapper jsonMapper = JsonUtils.getJsonMapper();
|
||||
JsonNode rootNode = jsonMapper.readTree(jsonParam);
|
||||
JsonUtils.removeFields(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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求完成后输出最终耗时,并清理线程内缓存的计时器。
|
||||
*
|
||||
* @param request 当前请求
|
||||
* @param response 当前响应
|
||||
* @param handler 目标处理器
|
||||
* @param ex 请求处理过程中的异常
|
||||
* @throws Exception 拦截器链路抛出的异常
|
||||
*/
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
|
||||
StopWatch stopWatch = KEY_CACHE.get();
|
||||
if (ObjectUtil.isNotNull(stopWatch)) {
|
||||
stopWatch.stop();
|
||||
log.info("[PLUS]结束请求 => URL[{}],耗时:[{}]毫秒", request.getMethod() + " " + request.getRequestURI(), stopWatch.getDuration().toMillis());
|
||||
KEY_CACHE.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断本次请求的数据类型是否为json
|
||||
*
|
||||
* @param request request
|
||||
* @return boolean
|
||||
*/
|
||||
private boolean isJsonRequest(HttpServletRequest request) {
|
||||
String contentType = request.getContentType();
|
||||
if (contentType != null) {
|
||||
return StringUtils.startsWithIgnoreCase(contentType, MediaType.APPLICATION_JSON_VALUE);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
org.dromara.common.web.config.FilterConfig
|
||||
org.dromara.common.web.config.I18nConfig
|
||||
org.dromara.common.web.config.ResourcesConfig
|
||||
Reference in New Issue
Block a user