feat(安全): 增加验证码和安全验证功能
refactor(XSS): 重构XSS过滤逻辑并添加JSON反序列化过滤 feat(防重放): 前端添加防重放攻击机制 fix(验证码): 优化验证码生成和异常处理 style: 格式化代码并修复部分警告
This commit is contained in:
@@ -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/**")
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ public class CaptchaUtil {
|
||||
cleanupExpiredCaptchas();
|
||||
|
||||
// 生成验证码ID
|
||||
String captchaId = UuidV7.generate().toString();
|
||||
String captchaId = UuidV7.uuid();
|
||||
|
||||
// 生成验证码字符
|
||||
String code = generateCode();
|
||||
|
||||
Reference in New Issue
Block a user