请求体处理
⚡ 解决 HttpServletRequest 输入流只能读取一次的问题,并提供 XSS 攻击防护能力。
- 🔄 流复用:
RequestBodyFilter支持请求体多次读取 - 🛡️ XSS 防护:
XssFilter自动过滤恶意脚本攻击 - 🎯 智能判断:仅对 JSON 请求进行包装,避免文件上传异常
- 🔧 灵活配置:支持自定义执行条件和排除规则
- 📦 自动启用:引入依赖后默认自动配置
- ⚙️ 可扩展:支持自定义 Safelist 和过滤策略
快速开始
自动启用
引入 krismile-boot3-autoconfigure 依赖后,请求体处理功能默认自动启用:
<dependency>
<groupId>host.springboot</groupId>
<artifactId>krismile-boot3-autoconfigure</artifactId>
</dependency>自动启用的功能:
- ✅
RequestBodyFilter- 请求体包装过滤器(仅 JSON 请求) - ✅
XssFilter- XSS 攻击防护过滤器(所有请求)
基础示例
场景1:多次读取请求体
@RestController
@RequestMapping("/api/user")
public class UserController {
@PostMapping("/create")
public BaseVO createUser(@RequestBody UserDTO userDTO, HttpServletRequest request)
throws IOException {
// 第一次读取请求体(在 @RequestBody 绑定时已读取)
// 通常情况下,第二次读取会抛出异常
// ✅ 有了 RequestBodyFilter,可以多次读取
String requestBody = new BufferedReader(
new InputStreamReader(request.getInputStream())
).lines().collect(Collectors.joining("\n"));
log.info("原始请求体: {}", requestBody);
// 继续处理业务逻辑
return R.ok(userService.createUser(userDTO));
}
}场景2:XSS 攻击自动防护
// 前端发送恶意脚本
{
"username": "test<script>alert('XSS')</script>",
"email": "test@example.com"
}
// ✅ XssFilter 自动清理后,Controller 接收到的数据
{
"username": "test", // <script> 标签已被移除
"email": "test@example.com"
}核心架构
工作流程
HTTP 请求
↓
┌─────────────────────────────────────┐
│ Filter Chain(按 Order 执行) │
├─────────────────────────────────────┤
│ 1. RequestBodyFilter (Order=-10000) │ ← 仅 JSON 请求
│ ↓ │
│ - 判断是否为 JSON 请求 │
│ - 是 → 包装为 RequestBodyWrapper │
│ - 否 → 跳过 │
│ │
│ 2. XssFilter (Order=-9000) │ ← 所有请求
│ ↓ │
│ - 包装为 XssWrapper │
│ - 过滤请求参数和请求头 │
│ │
│ 3. 其他过滤器... │
└─────────────────────────────────────┘
↓
Controller 方法执行核心类说明
| 类名 | 职责 | 说明 |
|---|---|---|
RequestBodyFilter | 请求体包装过滤器 | 解决输入流只能读取一次的问题 |
RequestBodyWrapper | 请求体包装器 | 缓存请求体字节数组,支持多次读取 |
XssFilter | XSS 攻击防护过滤器 | 过滤恶意脚本标签 |
XssWrapper | XSS 包装器 | 清理请求参数和请求头中的 XSS 脚本 |
HttpRequestUtils | 请求工具类 | 提供 isJsonRequest() 等工具方法 |
XssUtils | XSS 工具类 | 基于 Jsoup 的 XSS 清理工具 |
执行顺序
Filter 执行顺序(Order 数字越小越先执行):
1. RequestBodyFilter (ORDER = -10000)
↓
2. XssFilter (ORDER = -9000)
↓
3. 其他过滤器...
↓
4. Controller 方法执行💡 为什么 RequestBodyFilter 优先级最高?
- 必须在其他过滤器读取请求体之前完成包装
- 确保所有后续过滤器和 Controller 都能多次读取请求体
- 避免文件上传等场景下的冲突
RequestBodyFilter - 请求体包装
核心功能
解决问题:HttpServletRequest 的 InputStream 只能读取一次
实现原理:
- 拦截请求,判断是否为 JSON 请求(
Content-Type: application/json) - 读取原始输入流,缓存到字节数组
- 包装为
RequestBodyWrapper,提供基于字节数组的新输入流 - 后续可以多次调用
getInputStream()和getReader()
工作原理
// RequestBodyWrapper 核心实现
public class RequestBodyWrapper extends HttpServletRequestWrapper {
private final byte[] copyBody; // 缓存的请求体字节数组
public RequestBodyWrapper(HttpServletRequest request) throws IOException {
super(request);
// 一次性读取原始输入流并缓存
InputStream inputStream = request.getInputStream();
this.copyBody = copyInputStreamByte(inputStream);
}
@Override
public ServletInputStream getInputStream() {
// 返回基于缓存字节数组的新输入流
return new ServletInputStream() {
private int lastIndexRetrieved = -1;
@Override
public int read() throws IOException {
if (!isFinished()) {
int index = copyBody[lastIndexRetrieved + 1];
lastIndexRetrieved++;
return index;
}
return -1;
}
@Override
public boolean isFinished() {
return lastIndexRetrieved == (copyBody.length - 1);
}
// ... 其他方法
};
}
@Override
public BufferedReader getReader() {
// 返回基于缓存字节数组的新 Reader
return new BufferedReader(
new InputStreamReader(new ByteArrayInputStream(copyBody))
);
}
}仅处理 JSON 请求
为什么只处理 JSON 请求?
因为文件上传等表单请求会使用 request.getParts() 方法,该方法内部会读取输入流。如果提前包装了请求体,会导致文件上传失败。
判断逻辑:
public static boolean isJsonRequest(HttpServletRequest request) {
String contentType = request.getHeader("Content-Type");
return contentType != null &&
contentType.contains("application/json");
}请求类型对比:
| 请求类型 | Content-Type | 是否包装 | 原因 |
|---|---|---|---|
| JSON 请求 | application/json | ✅ 是 | 可以安全包装 |
| 表单请求 | application/x-www-form-urlencoded | ❌ 否 | 参数通过 getParameter() 获取 |
| 文件上传 | multipart/form-data | ❌ 否 | 避免影响 getParts() |
| 其他类型 | - | ❌ 否 | 不需要多次读取 |
启用与禁用
方式1:配置文件控制(推荐)
krismile:
web:
request-body-wrapper:
enabled: true # 默认启用方式2:自定义配置类
@Configuration
public class CustomFilterConfig {
// 方式1:完全禁用(不注册 Bean)
// 删除或注释掉 requestBodyFilter Bean
// 方式2:自定义执行条件
@Bean
public RequestBodyFilter.Config requestBodyFilterConfig() {
RequestBodyFilter.Config config = new RequestBodyFilter.Config();
// 默认只处理 JSON 请求
// 可以自定义执行条件
return config;
}
}XssFilter - XSS 攻击防护
核心功能
防护原理:基于 Jsoup 库的 Safelist 机制,清理 HTML 标签和脚本
防护范围:
- ✅ 请求参数(
request.getParameter()) - ✅ 请求头(
request.getHeader()) - ❌ 请求体(JSON)- 需要结合 @RequestBody 的校验
工作原理
// XssWrapper 核心实现
public class XssWrapper extends HttpServletRequestWrapper {
private final List<String> excludeParamEndWith; // 排除的参数名后缀
private final Safelist safeList; // 安全标签白名单
private final Document.OutputSettings outputSettings;
@Override
public String getParameter(String name) {
// 1. 检查是否排除该参数
if (excludeParamEndWith != null) {
for (String endWith : excludeParamEndWith) {
if (name.endsWith(endWith)) {
return super.getParameter(name); // 不过滤
}
}
}
// 2. 清理参数名
name = XssUtils.clean(name, safeList, outputSettings);
// 3. 清理参数值
String value = super.getParameter(name);
if (StringUtils.isNotBlank(value)) {
value = XssUtils.clean(value, safeList, outputSettings);
}
return value;
}
@Override
public String getHeader(String name) {
// 同样清理请求头名称和值
name = XssUtils.clean(name, safeList, outputSettings);
String value = super.getHeader(name);
if (StringUtils.isNotBlank(value)) {
value = XssUtils.clean(value, safeList, outputSettings);
}
return value;
}
}XSS 清理示例
示例1:清理脚本标签
// 输入
String input = "Hello<script>alert('XSS')</script>World";
// 使用默认 Safelist.none()(不允许任何 HTML 标签)
String output = XssUtils.clean(input);
// 输出
"HelloWorld" // <script> 标签被完全移除示例2:允许特定 HTML 标签
// 输入
String input = "<p>Hello</p><script>alert('XSS')</script><b>World</b>";
// 允许 <p> 和 <b> 标签
Safelist safelist = Safelist.basic();
String output = XssUtils.clean(input, safelist);
// 输出
"<p>Hello</p><b>World</b>" // 只保留安全标签示例3:排除特定参数
// 配置排除 "content" 和 "html" 结尾的参数
XssFilter.Config config = new XssFilter.Config()
.setExcludeParamEndWith(Arrays.asList("content", "html"));
// 请求:?username=<script>alert()</script>&articleContent=<p>文章内容</p>
// 结果:
// username = "" (被清理)
// articleContent = "<p>文章内容</p>" (不清理)Safelist 配置
Jsoup 提供了多种预定义的 Safelist:
| Safelist | 允许的标签 | 适用场景 |
|---|---|---|
Safelist.none() | 不允许任何 HTML | 默认,最严格,推荐 |
Safelist.simpleText() | 仅允许 <b>, <em>, <i>, <strong>, <u> | 简单文本格式 |
Safelist.basic() | 允许基本格式标签 | 评论、简单富文本 |
Safelist.basicWithImages() | 基本标签 + 图片 | 支持图片的富文本 |
Safelist.relaxed() | 允许大部分标签 | 完整富文本编辑器 |
自定义 Safelist:
@Bean
public XssFilter.Config xssFilterConfig() {
// 创建自定义白名单
Safelist safelist = new Safelist()
.addTags("p", "br", "b", "i", "strong", "em") // 允许的标签
.addAttributes("a", "href", "title") // 允许 a 标签的属性
.addProtocols("a", "href", "http", "https"); // 允许的协议
return new XssFilter.Config()
.setSafeList(safelist)
.setExcludeParamEndWith(Arrays.asList("content", "html"));
}启用与禁用
方式1:配置文件控制(推荐)
krismile:
web:
xss:
enabled: true # 默认启用方式2:自定义配置
@Configuration
public class XssConfig {
@Bean
public XssFilter.Config xssFilterConfig() {
return new XssFilter.Config()
// 自定义执行条件(默认处理所有请求)
.setExecutePredicate(request -> {
// 仅对特定路径启用 XSS 过滤
String uri = request.getRequestURI();
return uri.startsWith("/api/public/");
})
// 排除特定参数
.setExcludeParamEndWith(Arrays.asList(
"content", // 文章内容
"html", // HTML 内容
"richText" // 富文本
))
// 自定义安全标签白名单
.setSafeList(Safelist.basic())
// 输出配置(禁用格式化)
.setOutputSettings(
new Document.OutputSettings().prettyPrint(false)
);
}
}常见场景
场景1:AOP 日志记录
在 AOP 中读取请求体用于日志记录:
@Aspect
@Component
public class RequestLogAspect {
@Around("@annotation(org.springframework.web.bind.annotation.PostMapping)")
public Object logRequest(ProceedingJoinPoint point) throws Throwable {
HttpServletRequest request =
((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest();
// ✅ 有了 RequestBodyFilter,可以在 AOP 中读取请求体
if (HttpRequestUtils.isJsonRequest(request)) {
String requestBody = new BufferedReader(
new InputStreamReader(request.getInputStream())
).lines().collect(Collectors.joining("\n"));
log.info("请求体: {}", requestBody);
}
// Controller 中的 @RequestBody 仍然可以正常绑定
return point.proceed();
}
}场景2:接口签名验证
在过滤器中读取请求体进行签名验证:
@Component
public class SignatureFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
if (HttpRequestUtils.isJsonRequest(request)) {
// ✅ 读取请求体计算签名
String requestBody = new BufferedReader(
new InputStreamReader(request.getInputStream())
).lines().collect(Collectors.joining("\n"));
String signature = request.getHeader("X-Signature");
// 验证签名
if (!verifySignature(requestBody, signature)) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return;
}
}
// ✅ Controller 中仍然可以正常使用 @RequestBody
filterChain.doFilter(request, response);
}
private boolean verifySignature(String body, String signature) {
// 签名验证逻辑
return true;
}
}场景3:富文本编辑器内容保存
某些字段需要保留 HTML 标签:
// 配置排除规则
@Bean
public XssFilter.Config xssFilterConfig() {
return new XssFilter.Config()
// 排除富文本字段
.setExcludeParamEndWith(Arrays.asList(
"Content", // 文章内容
"Html", // HTML 内容
"RichText" // 富文本
))
// 或使用自定义 Safelist 允许特定标签
.setSafeList(Safelist.relaxed());
}
// Controller
@PostMapping("/article/create")
public BaseVO createArticle(@RequestBody ArticleDTO dto) {
// dto.getArticleContent() 保留了 HTML 标签
// dto.getTitle() 被 XSS 过滤
return R.ok(articleService.create(dto));
}场景4:API 网关统一日志
在网关层记录所有请求体:
@Component
public class GatewayRequestLogFilter implements Filter {
@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException, ServletException {
if (request instanceof HttpServletRequest httpRequest) {
if (HttpRequestUtils.isJsonRequest(httpRequest)) {
// ✅ 读取请求体用于网关日志
String requestBody = new BufferedReader(
new InputStreamReader(httpRequest.getInputStream())
).lines().collect(Collectors.joining("\n"));
// 记录到 ELK
logToElk(httpRequest.getRequestURI(), requestBody);
}
}
// ✅ 下游服务仍可正常读取请求体
chain.doFilter(request, response);
}
private void logToElk(String uri, String body) {
// 发送到 ELK
}
}场景5:敏感数据脱敏
在日志中脱敏敏感字段:
@Aspect
@Component
public class DataMaskingAspect {
@Around("execution(* com.example.controller..*(..))")
public Object maskSensitiveData(ProceedingJoinPoint point) throws Throwable {
HttpServletRequest request =
((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest();
if (HttpRequestUtils.isJsonRequest(request)) {
// ✅ 读取请求体
String requestBody = new BufferedReader(
new InputStreamReader(request.getInputStream())
).lines().collect(Collectors.joining("\n"));
// 脱敏处理
String maskedBody = maskSensitiveFields(requestBody);
log.info("请求体(已脱敏): {}", maskedBody);
}
return point.proceed();
}
private String maskSensitiveFields(String body) {
// 脱敏手机号、身份证等
return body.replaceAll("\"phone\":\"(\\d{3})\\d{4}(\\d{4})\"",
"\"phone\":\"$1****$2\"");
}
}最佳实践
✅ 推荐做法
// 1. 使用工具方法判断是否为 JSON 请求
if (HttpRequestUtils.isJsonRequest(request)) {
// 安全读取请求体
String body = readRequestBody(request);
}
// 2. 读取请求体后使用 try-with-resources
try (BufferedReader reader = request.getReader()) {
String body = reader.lines().collect(Collectors.joining("\n"));
// 处理请求体
}
// 3. 富文本字段配置排除规则
@Bean
public XssFilter.Config xssFilterConfig() {
return new XssFilter.Config()
.setExcludeParamEndWith(Arrays.asList(
"Content", "Html", "RichText"
));
}
// 4. 生产环境使用严格的 Safelist
@Bean
@Profile("prod")
public XssFilter.Config prodXssConfig() {
return new XssFilter.Config()
.setSafeList(Safelist.none()); // 最严格
}
// 5. 自定义执行条件,减少不必要的包装
@Bean
public XssFilter.Config xssFilterConfig() {
return new XssFilter.Config()
.setExecutePredicate(request -> {
// 仅对 /api/ 开头的接口启用
return request.getRequestURI().startsWith("/api/");
});
}❌ 不推荐做法
// ❌ 1. 对非 JSON 请求读取输入流
if (!HttpRequestUtils.isJsonRequest(request)) {
// 错误:表单请求、文件上传不应读取输入流
String body = readRequestBody(request);
}
// ✅ 正确做法
if (HttpRequestUtils.isJsonRequest(request)) {
String body = readRequestBody(request);
}
// ❌ 2. 忘记关闭流
BufferedReader reader = request.getReader();
String body = reader.lines().collect(Collectors.joining("\n"));
// reader 未关闭
// ✅ 正确做法
try (BufferedReader reader = request.getReader()) {
String body = reader.lines().collect(Collectors.joining("\n"));
}
// ❌ 3. 对所有字段都使用 Safelist.relaxed()
@Bean
public XssFilter.Config xssFilterConfig() {
return new XssFilter.Config()
.setSafeList(Safelist.relaxed()); // 太宽松,不安全
}
// ✅ 正确做法:仅对需要的字段使用宽松策略
@Bean
public XssFilter.Config xssFilterConfig() {
return new XssFilter.Config()
.setSafeList(Safelist.none()) // 默认严格
.setExcludeParamEndWith(Arrays.asList("Content")); // 特定字段排除
}
// ❌ 4. 在 Controller 中手动读取请求体
@PostMapping("/create")
public BaseVO create(HttpServletRequest request) throws IOException {
// 错误:手动读取,无法使用 @RequestBody
String body = request.getReader().lines()
.collect(Collectors.joining("\n"));
UserDTO dto = JSON.parseObject(body, UserDTO.class);
return R.ok(userService.create(dto));
}
// ✅ 正确做法:使用 @RequestBody
@PostMapping("/create")
public BaseVO create(@RequestBody UserDTO dto) {
// Spring 自动绑定
return R.ok(userService.create(dto));
}
// ❌ 5. 禁用 XSS 过滤器来"解决"问题
krismile:
web:
xss:
enabled: false # 错误:完全禁用不安全
// ✅ 正确做法:配置排除规则
krismile:
web:
xss:
enabled: true
@Bean
public XssFilter.Config xssFilterConfig() {
return new XssFilter.Config()
.setExcludeParamEndWith(Arrays.asList("Content"));
}配置参考
完整配置示例
application.yml:
krismile:
web:
# 请求体包装过滤器
request-body-wrapper:
enabled: true # 默认启用
# XSS 攻击防护过滤器
xss:
enabled: true # 默认启用自定义配置类
@Configuration
public class FilterCustomConfig {
/**
* 自定义请求体包装配置
*/
@Bean
public RequestBodyFilter.Config requestBodyFilterConfig() {
// 默认配置:仅处理 JSON 请求
return new RequestBodyFilter.Config();
// 自定义配置示例:
// return new RequestBodyFilter.Config() {
// @Override
// public Predicate<HttpServletRequest> getExecutePredicate() {
// return request -> {
// // 自定义判断逻辑
// return HttpRequestUtils.isJsonRequest(request);
// };
// }
// };
}
/**
* 自定义 XSS 过滤配置
*/
@Bean
public XssFilter.Config xssFilterConfig() {
// 创建自定义白名单
Safelist safelist = new Safelist()
.addTags("p", "br", "b", "i", "strong", "em")
.addAttributes("a", "href", "title")
.addProtocols("a", "href", "http", "https");
return new XssFilter.Config()
// 执行条件:仅对 /api/ 开头的接口启用
.setExecutePredicate(request ->
request.getRequestURI().startsWith("/api/")
)
// 排除富文本字段
.setExcludeParamEndWith(Arrays.asList(
"Content",
"Html",
"RichText",
"Description"
))
// 使用自定义白名单
.setSafeList(safelist)
// 输出配置
.setOutputSettings(
new Document.OutputSettings().prettyPrint(false)
);
}
}环境差异化配置
// 开发环境:宽松配置
@Configuration
@Profile("dev")
public class DevFilterConfig {
@Bean
public XssFilter.Config xssFilterConfig() {
return new XssFilter.Config()
.setSafeList(Safelist.relaxed()) // 宽松
.setExcludeParamEndWith(Arrays.asList(
"Content", "Html"
));
}
}
// 生产环境:严格配置
@Configuration
@Profile("prod")
public class ProdFilterConfig {
@Bean
public XssFilter.Config xssFilterConfig() {
return new XssFilter.Config()
.setSafeList(Safelist.none()) // 最严格
.setExcludeParamEndWith(Arrays.asList(
"articleContent" // 仅文章内容排除
));
}
}常见问题
Q: 为什么文件上传失败了?
A: RequestBodyFilter 默认只处理 JSON 请求,不会影响文件上传。如果遇到问题,检查:
// 检查是否自定义了执行条件
@Bean
public RequestBodyFilter.Config requestBodyFilterConfig() {
return new RequestBodyFilter.Config();
// 默认只处理 JSON 请求,不影响文件上传
}
// 如果自定义了执行条件,确保排除文件上传请求
@Bean
public RequestBodyFilter.Config requestBodyFilterConfig() {
return new RequestBodyFilter.Config() {
@Override
public Predicate<HttpServletRequest> getExecutePredicate() {
return request -> {
// ✅ 仅处理 JSON 请求
return HttpRequestUtils.isJsonRequest(request);
// ❌ 错误:处理所有请求会导致文件上传失败
// return true;
};
}
};
}Q: 如何在 AOP 中读取请求体?
A: 框架已经自动启用了 RequestBodyFilter,可以直接读取:
@Around("execution(* com.example.controller..*(..))")
public Object logRequest(ProceedingJoinPoint point) throws Throwable {
HttpServletRequest request = getCurrentRequest();
// ✅ 直接读取(框架已自动包装)
if (HttpRequestUtils.isJsonRequest(request)) {
try (BufferedReader reader = request.getReader()) {
String body = reader.lines().collect(Collectors.joining("\n"));
log.info("请求体: {}", body);
}
}
// Controller 中的 @RequestBody 仍可正常使用
return point.proceed();
}Q: XSS 过滤器会影响 JSON 请求体吗?
A: 不会。XssFilter 只过滤以下内容:
- ✅ 请求参数(
request.getParameter()) - ✅ 请求头(
request.getHeader()) - ❌ 请求体(JSON)- 不过滤
对于 JSON 请求体,建议使用 Bean Validation:
public class UserDTO {
@NotBlank
@Pattern(regexp = "^[a-zA-Z0-9_-]{3,20}$", message = "用户名格式错误")
private String username;
// 自定义校验器过滤 XSS
@NoXss // 自定义注解
private String description;
}Q: 如何保留富文本内容的 HTML 标签?
A: 有两种方式:
方式1:配置排除规则(推荐)
@Bean
public XssFilter.Config xssFilterConfig() {
return new XssFilter.Config()
.setExcludeParamEndWith(Arrays.asList(
"Content", // articleContent、richContent 等
"Html", // contentHtml、bodyHtml 等
"RichText" // richText、richTextContent 等
));
}方式2:使用宽松的 Safelist
@Bean
public XssFilter.Config xssFilterConfig() {
return new XssFilter.Config()
.setSafeList(Safelist.relaxed()); // 允许大部分 HTML 标签
}Q: 如何禁用请求体包装或 XSS 过滤?
A: 通过配置文件控制:
krismile:
web:
# 禁用请求体包装
request-body-wrapper:
enabled: false
# 禁用 XSS 过滤
xss:
enabled: falseQ: RequestBodyFilter 的性能影响?
A: 性能影响很小:
- 仅处理 JSON 请求:表单、文件上传不受影响
- 内存占用:缓存请求体到字节数组,通常只有几 KB
- 执行时间:额外读取一次流,耗时 < 1ms
优化建议:
// 如果请求体很大(> 10MB),可以限制缓存大小
public class RequestBodyWrapper extends HttpServletRequestWrapper {
private static final int MAX_BODY_SIZE = 10 * 1024 * 1024; // 10MB
public RequestBodyWrapper(HttpServletRequest request) throws IOException {
super(request);
int contentLength = request.getContentLength();
if (contentLength > MAX_BODY_SIZE) {
throw new IllegalArgumentException("请求体过大");
}
// ... 读取请求体
}
}Q: XssUtils.clean() 的清理规则?
A: 基于 Jsoup 的 Safelist 机制:
// 默认:Safelist.none() - 移除所有 HTML 标签
XssUtils.clean("<script>alert()</script>Hello");
// 输出: "Hello"
XssUtils.clean("<p>Hello</p><b>World</b>");
// 输出: "HelloWorld"
// 自定义:Safelist.basic() - 允许基本格式标签
XssUtils.clean("<p>Hello</p><script>alert()</script>", Safelist.basic());
// 输出: "<p>Hello</p>" (保留 <p>,移除 <script>)完整规则:
- 移除所有不在白名单中的标签
- 移除标签的不安全属性(如
onclick) - 移除不安全的协议(如
javascript:) - 编码特殊字符(如
<,>)
Q: 如何自定义 Safelist?
A: 使用链式调用构建:
Safelist safelist = new Safelist()
// 允许的标签
.addTags("p", "br", "span", "div", "b", "i", "strong", "em")
// 允许的属性
.addAttributes("a", "href", "title")
.addAttributes("img", "src", "alt", "width", "height")
.addAttributes("span", "class", "style")
// 允许的协议
.addProtocols("a", "href", "http", "https")
.addProtocols("img", "src", "http", "https")
// 允许的 CSS 属性
.addEnforcedAttribute("a", "rel", "nofollow");
XssFilter.Config config = new XssFilter.Config()
.setSafeList(safelist);💡 提示:
- RequestBodyFilter 解决的是技术问题(流只能读取一次)
- XssFilter 解决的是安全问题(防护 XSS 攻击)
- 两者配合使用,提供完整的请求处理能力
- 生产环境务必启用 XSS 过滤,使用严格的 Safelist
