项目里需要做 XSS 防护,自己从头写一个过滤器也不是不行,但想到若依(RuoYi)作为国内最常用的 Spring Boot 脚手架之一,肯定有现成的实现。翻了下源码,确实有,而且设计得挺清晰——配置层、过滤器层、请求包装层、过滤引擎层各司其职。直接移植过来了,这里记录一下源码分析。
1. FilterConfig — 配置入口
@Configuration
public class FilterConfig
{
@Value("${xss.excludes}")
private String excludes;
@Value("${xss.urlPatterns}")
private String urlPatterns;
@SuppressWarnings({ "rawtypes", "unchecked" })
@Bean
@ConditionalOnProperty(value = "xss.enabled", havingValue = "true")
public FilterRegistrationBean xssFilterRegistration()
{
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setDispatcherTypes(DispatcherType.REQUEST);
registration.setFilter(new XssFilter());
registration.addUrlPatterns(StringUtils.split(urlPatterns, ","));
registration.setName("xssFilter");
registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
Map<String, String> initParameters = new HashMap<String, String>();
initParameters.put("excludes", excludes);
registration.setInitParameters(initParameters);
return registration;
}
}
几个设计要点:
@ConditionalOnProperty(value = "xss.enabled", havingValue = "true")— XSS 过滤可通过配置开关,不需要时直接关掉FilterRegistrationBean.HIGHEST_PRECEDENCE— 最高优先级,确保 XSS 过滤在其他过滤器之前执行- URL 模式从配置文件注入,例如
/system/*,/monitor/*,/tool/* - 排除 URL 列表也通过配置文件注入,例如
/system/notice
urlPatterns 和 excludes 都支持逗号分隔的多值配置,用 StringUtils.split 解析。
2. XssFilter — 过滤器本身
public class XssFilter implements Filter
{
public List<String> excludes = new ArrayList<>();
@Override
public void init(FilterConfig filterConfig) throws ServletException
{
String tempExcludes = filterConfig.getInitParameter("excludes");
if (StringUtils.isNotEmpty(tempExcludes))
{
String[] urls = tempExcludes.split(",");
for (String url : urls)
{
excludes.add(url);
}
}
}
@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);
}
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);
}
}
核心逻辑在 doFilter:
- 检查当前请求是否在排除列表中
- GET 和 DELETE 请求直接放行(这两种方法不应该携带需要过滤的请求体)
- 不在排除列表的请求,用
XssHttpServletRequestWrapper包装后继续执行过滤器链
这里有个细节:排除逻辑同时检查了 HTTP 方法和 URL 匹配,两个条件任一命中就跳过过滤。
3. XssHttpServletRequestWrapper — 请求包装
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper
{
public XssHttpServletRequestWrapper(HttpServletRequest request)
{
super(request);
}
@Override
public String[] getParameterValues(String name)
{
String[] values = super.getParameterValues(name);
if (values != null)
{
int length = values.length;
String[] escapesValues = new String[length];
for (int i = 0; i < length; i++)
{
escapesValues[i] = EscapeUtil.clean(values[i]).trim();
}
return escapesValues;
}
return super.getParameterValues(name);
}
@Override
public ServletInputStream getInputStream() throws IOException
{
// 非 JSON 类型,直接返回原输入流
if (!isJsonRequest())
{
return super.getInputStream();
}
String json = IOUtils.toString(super.getInputStream(), "utf-8");
if (StringUtils.isEmpty(json))
{
return super.getInputStream();
}
// XSS 过滤
json = EscapeUtil.clean(json).trim();
byte[] jsonBytes = json.getBytes("utf-8");
final ByteArrayInputStream bis = new ByteArrayInputStream(jsonBytes);
return new ServletInputStream()
{
@Override
public boolean isFinished() { return true; }
@Override
public boolean isReady() { return true; }
@Override
public int available() throws IOException { return jsonBytes.length; }
@Override
public void setReadListener(ReadListener readListener) {}
@Override
public int read() throws IOException { return bis.read(); }
};
}
public boolean isJsonRequest()
{
String header = super.getHeader(HttpHeaders.CONTENT_TYPE);
return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE);
}
}
这个类继承 HttpServletRequestWrapper,覆盖了两个入口:
getParameterValues:过滤 URL 参数和表单参数(键值对形式的参数)getInputStream:过滤 JSON 请求体。先判断Content-Type是否为 JSON,是的话把整个 body 读出来过滤后再塞回去
两处过滤都调了 EscapeUtil.clean(),核心过滤逻辑都在那一个方法里。
4. EscapeUtil — 转义工具
public class EscapeUtil
{
public static final String RE_HTML_MARK = "(<[^<]*?>)|(<[\\s]*?/[^<]*?>)|(<[^<]*?/[\\s]*?>)";
private static final char[][] TEXT = new char[64][];
static
{
for (int i = 0; i < 64; i++)
{
TEXT[i] = new char[] { (char) i };
}
TEXT['\''] = "'".toCharArray(); // 单引号
TEXT['"'] = """.toCharArray(); // 双引号
TEXT['&'] = "&".toCharArray(); // &符
TEXT['<'] = "<".toCharArray(); // 小于号
TEXT['>'] = ">".toCharArray(); // 大于号
}
public static String clean(String content)
{
return new HTMLFilter().filter(content);
}
}
XSS 过滤实际只用到 clean 这一个方法,它委托给 HTMLFilter.filter()。
EscapeUtil 还有 escape(URL 编码)和 unescape(解码)方法,但在 XSS 场景下用不到。
5. HTMLFilter — 核心过滤引擎
这是最大的一坨代码,也是 XSS 防护的核心。完整的 filter 流程:
public String filter(final String input)
{
reset(); // 清空标签计数
String s = input;
s = escapeComments(s); // 1. 转义注释中的特殊字符
s = balanceHTML(s); // 2. 平衡/转义不成对的尖括号
s = checkTags(s); // 3. 白名单标签和属性过滤
s = processRemoveBlanks(s); // 4. 移除空标签
return s;
}
第一步 checkTags 是核心中的核心——它用正则匹配出每个 HTML 标签,然后按白名单规则处理:
- 不在白名单的标签 → 直接移除
- 在白名单的标签 → 属性按白名单过滤,协议属性(
src、href)额外检查协议白名单(只允许http、https、mailto)
默认白名单很精简,只允许这几个标签和对应属性:
| 标签 | 允许的属性 |
|---|---|
a |
href, target |
img |
src, width, height, alt |
b, strong, i, em |
无 |
第二步 processRemoveBlanks 清理空标签,比如 <b></b>、<b /> 这类没有任何内容的标签直接干掉。
第三步 balanceHTML 处理不成对的尖括号——默认模式 alwaysMakeTags = false,把不成对的 < > 直接转义成 < >,防止绕过。
整体数据流
请求进来
→ FilterConfig 判断 xss.enabled 是否开启
→ XssFilter.doFilter() 判断是否排除 URL / GET&DELETE 请求
→ XssHttpServletRequestWrapper 包装请求
→ getParameterValues() 过滤 URL/表单参数
→ getInputStream() 过滤 JSON 请求体
→ EscapeUtil.clean()
→ HTMLFilter.filter() 白名单过滤
说实话移植过程中最花时间的是理解 HTMLFilter 的正则那坨——这玩意是从 PHP 的 XSS 过滤库翻译过来的,逻辑是对的但代码风格一看就是 PHP 转 Java,挺多冗余。不过能用就行,没必要自己重写一个。
XSS 过滤器在 application.yml 中的配置示例:
xss:
enabled: true
urlPatterns: /system/*,/monitor/*,/tool/*
excludes: /system/notice
