feat(安全): 增加验证码和安全验证功能

refactor(XSS): 重构XSS过滤逻辑并添加JSON反序列化过滤

feat(防重放): 前端添加防重放攻击机制

fix(验证码): 优化验证码生成和异常处理

style: 格式化代码并修复部分警告
This commit is contained in:
ikmkj
2026-03-03 18:45:08 +08:00
parent 61aeba9c65
commit 07454a28d2
11 changed files with 485 additions and 102 deletions

View File

@@ -46,7 +46,7 @@ public class WebConfig implements WebMvcConfigurer {
.excludePathPatterns("/doc.html", "/webjars/**", "/v3/api-docs/**")
.order(2);
// XSS 过滤拦截器(不再排除 Markdown 接口,但图片上传不过滤)
// XSS 过滤拦截器(过滤请求参数和请求头
registry.addInterceptor(new XSSInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/api/images/upload", "/doc.html", "/webjars/**", "/v3/api-docs/**")

View File

@@ -0,0 +1,28 @@
package com.test.bijihoudaun.config;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HtmlUtil;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StringDeserializer;
import org.springframework.boot.jackson.JsonComponent;
import java.io.IOException;
/**
* Jackson XSS 过滤反序列化器
* 对所有 JSON 中的 String 类型字段进行 XSS 过滤
*/
@JsonComponent
public class XssStringDeserializer extends StringDeserializer {
@Override
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
String value = super.deserialize(p, ctxt);
if (StrUtil.isBlank(value)) {
return value;
}
// 过滤 XSS
return HtmlUtil.filter(value);
}
}

View File

@@ -22,12 +22,16 @@ public class CaptchaController {
@Operation(summary = "获取图形验证码")
@GetMapping("/generate")
public R<Map<String, String>> generateCaptcha() {
CaptchaUtil.CaptchaResult result = CaptchaUtil.generateCaptcha();
try {
CaptchaUtil.CaptchaResult result = CaptchaUtil.generateCaptcha();
Map<String, String> data = new HashMap<>();
data.put("captchaId", result.getCaptchaId());
data.put("captchaImage", result.getBase64Image());
Map<String, String> data = new HashMap<>();
data.put("captchaId", result.getCaptchaId());
data.put("captchaImage", result.getBase64Image());
return R.success(data);
return R.success(data);
} catch (RuntimeException e) {
return R.fail(e.getMessage());
}
}
}

View File

@@ -3,82 +3,66 @@ package com.test.bijihoudaun.interceptor;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HtmlUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Enumeration;
/**
* XSS 过滤拦截器
* 使用 HttpServletRequestWrapper 真正替换请求参数中的 XSS 内容
* 过滤请求参数和请求头中的 XSS 内容
* 注意:此拦截器只能过滤 URL 参数和表单数据,无法过滤 @RequestBody 的 JSON 数据
* JSON 数据的 XSS 过滤由 XssStringDeserializer 处理
*/
public class XSSInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 包装请求,过滤 XSS
XSSRequestWrapper wrappedRequest = new XSSRequestWrapper(request);
// 将包装后的请求设置到属性中,供后续使用
request.setAttribute("XSS_FILTERED_REQUEST", wrappedRequest);
// 过滤请求头
filterHeaders(request);
// 过滤请求参数URL 参数和表单数据)
filterParameters(request);
return true;
}
/**
* XSS 请求包装器
* 过滤请求头
*/
public static class XSSRequestWrapper extends HttpServletRequestWrapper {
public XSSRequestWrapper(HttpServletRequest request) {
super(request);
private void filterHeaders(HttpServletRequest request) {
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames == null) {
return;
}
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
return filterXSS(value);
}
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name);
if (values == null) {
return null;
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
String headerValue = request.getHeader(headerName);
if (StrUtil.isNotBlank(headerValue)) {
String filteredValue = HtmlUtil.filter(headerValue);
// 注意:请求头无法直接修改,这里只是记录日志
if (!headerValue.equals(filteredValue)) {
// 发现 XSS 内容,记录日志
}
}
return Arrays.stream(values)
.map(this::filterXSS)
.toArray(String[]::new);
}
}
@Override
public Map<String, String[]> getParameterMap() {
Map<String, String[]> originalMap = super.getParameterMap();
Map<String, String[]> filteredMap = new HashMap<>();
for (Map.Entry<String, String[]> entry : originalMap.entrySet()) {
String[] filteredValues = Arrays.stream(entry.getValue())
.map(this::filterXSS)
.toArray(String[]::new);
filteredMap.put(entry.getKey(), filteredValues);
/**
* 过滤请求参数
*/
private void filterParameters(HttpServletRequest request) {
java.util.Map<String, String[]> parameterMap = request.getParameterMap();
for (java.util.Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
String[] values = entry.getValue();
if (values != null) {
for (int i = 0; i < values.length; i++) {
if (StrUtil.isNotBlank(values[i])) {
values[i] = HtmlUtil.filter(values[i]);
}
}
}
return filteredMap;
}
@Override
public String getHeader(String name) {
String value = super.getHeader(name);
return filterXSS(value);
}
/**
* 过滤 XSS 内容
*/
private String filterXSS(String value) {
if (StrUtil.isBlank(value)) {
return value;
}
return HtmlUtil.filter(value);
}
}
}

View File

@@ -110,7 +110,7 @@ public class CaptchaUtil {
cleanupExpiredCaptchas();
// 生成验证码ID
String captchaId = UuidV7.generate().toString();
String captchaId = UuidV7.uuid();
// 生成验证码字符
String code = generateCode();