做Web开发这些年我一直在关注一个问题业务代码里写得清清楚楚的接口一旦过了安全扫描和渗透测试经常会被爆出一堆“反射型XSS”或者“存储型XSS”的漏洞。尤其是SpringMVC项目很多人习惯在Controller里手动用工具类替换特殊字符但漏掉的地方远比堵上的多。后来我把XSS过滤器Filter这套方案完整落地到项目里又踩了一遍multipart/form-data这个大坑才发现很多人说的“过滤器能搞定一切”其实只说对了一半。这篇文章就围绕SpringMVC下XSS过滤器Filter的实战实现重点讲清楚为什么普通包装器处理不了文件上传表单以及用哪种方式才能真正把multipart请求里的文本字段过滤干净。内容适合正在做安全整改、离职交接时接手老项目、或者准备把安全加固做成通用工程化的Java后端开发前端和后端对这个问题的理解完全是一个量级阅读本文不需要你精通Servlet但至少要知道Filter和拦截器大概是干什么的。1. XSS过滤器该管到哪一层1.1 过滤器和业务代码的本质区别很多人把XSS防护做成了“哪里漏了补哪里”登录接口发现username没过滤就加一个replace评论接口发现content有问题再写一段正则。这种做法的核心问题是你永远不知道下一个被攻击的参数长什么样而且只要有一个接口漏掉整个防线就等于没有。我在实际项目中更推荐把XSS过滤做成全局的、隔离于业务代码之外的一层通用处理。它的核心逻辑非常简单请求到达Controller之前统一清洗客户端提交的数据。你不需要在每个方法里写过滤代码也不需要依靠程序员的自觉。从Servlet体系来看Filter正好能承担这个职责。它的生命周期由容器管理天然运行在Servlet处理之前而且能拿到原始的ServletRequest和ServletResponse对象。你可以在过滤器中重新包装request对象让后续所有读取参数、读取头部的操作都经过清洗后的数据这就是“统一入口”的思路。1.2 为什么有人绕过Filter也能拿到脏数据一个常见的误区是只要写了XSS Filter所有的input就都安全了。实际并不是这样。Filter只能过滤它“看到”的数据。也就是说如果你的包装器只重写了getParameter()却没有重写getParameterMap()而业务代码里用了request.getParameterMap()取出整个Map再遍历那么过滤就失效了。同理JSON请求体、multipart文件字段、Header里的攻击向量也都是容易漏掉的点。所以在动手之前先想清楚你过滤的对象是什么。最粗粒度是字符串参数往上还有参数数组、参数Map、Header、InputStream、Reader。理论上来讲过滤得越全面越安全但也要考虑性能和代码的复杂度。1.3 multipart/form-data 为何成为XSS过滤的“重灾区”大多数Filter实现的Demo都是把HttpServletRequest包装成一个带清洗能力的wrapper然后重写getParameter()。这个方案处理普通的application/x-www-form-urlencoded请求没问题因为这种格式的请求体Servlet容器已经帮你解析好了request.getParameter()可以直接拿到表单值。但遇到multipart/form-data请求事情就不一样了。这种格式主要用于文件上传请求体会被boundary分隔成多个部分每个部分有自己的Content-Disposition和Content-Type。Servlet容器不会像普通表单那样把multipart直接解析成可读的参数至少在标准的ServletRequest层面参数获取方式是不一样的。SpringMVC里的MultipartResolver也是在框架层面解析的并不是Servlet规范默认就支持的。这就导致了一个很尴尬的场景你辛辛苦苦写好了XSS过滤器测试时发现enctypemultipart/form-data表单里的文本字段比如昵称、文件名、备注说明依然能塞入HTML标签和javascript:开头的危险代码。而这些字段都是进攻者可以自由控制的不处理等于这类型接口完全裸奔。2. 方案选型Filter、拦截器还是ControllerAdvice2.1 三种方案的取舍对比做技术方案选型的时候我习惯先把限制条件列出来要统一处理SpringMVC项目要支持老项目快速接入不能因为安全过滤影响正常业务。先说拦截器Interceptor。它是SpringMVC框架层的组件能拿到HandlerMethod甚至能判断当前请求走的是哪个Controller的哪个方法定位非常精准。但它拿不到原始Servlet请求流也不能像Filter那样直接替换掉Servlet规范层面的request对象。如果你想在拦截器里对请求体做清洗要么再次读取输入流此时可能已经被Spring读过了要么只处理Header和URL参数。所以它更适合做权限控制和日志记录不适合做请求体的强制清洗。再说ControllerAdvice。这个方案目前很流行结合InitBinder里的WebDataBinder可以注册自定义编辑器或者用RequestBodyAdvice去拦截JSON请求体。这种方式对JSON请求很友好因为你可以拿到字节流和对象清洗后再放行。但是它的作用范围受限于SpringMVC的请求处理链路对非Controller路径、静态资源、Servlet层面的转发请求就无能为力而且处理multipart请求体时还要兼顾MultipartFile的解码逻辑。作为一层通杀过滤它不够“前置”。最后回到Filter。它的最大优势是处于Servlet容器的最前端离原始请求最近。你可以替换HttpServletRequest把清洗能力注入到参数读取的每一个方法里让Controller层完全无感。代价是写起来更底层尤其是处理InputStream时需要自己Buffering稍有不慎就会把请求体读坏。我在真实项目里最终选了Filter因为它符合这个需求的核心目标作为一道通用安全防线放在最前面挡住所有进入SpringMVC的请求。2.2 过滤器方案的整体架构设计实现一个可用的XSS Filter要拆成三块第一块是清洗引擎。它负责定义哪些字符是危险的哪些标签会被放行清洗到什么程度不影响业务。第二块是请求包装器。它对HttpServletRequest做增强让容器和SpringMVC在读取数据时拿到的都是清洗后的值。第三块是Filter入口。它根据请求类型普通表单、JSON、multipart决定采用哪种包装策略并保证请求体被正确读取和缓存。这三块缺一不可。很多人写Filter只写了入口和包装器清洗逻辑直接用死板的正则替换结果把正常的富文本内容也搞坏了一上线就被业务方投诉。2.3 通用过滤策略白名单永远优于黑名单清洗XSS是一个比拼“谁更懂攻击手法”的游戏所以做规则设计时我的建议是尽量不要只在黑名单里加几个关键词而是要有白名单的思维。在实际项目里我采用了“三层递进”策略第一层对危险的HTML标签做白名单校验只允许常用的p、span、a、img等基础标签存在其余标签一律转义或清除。第二层对属性做校验比如onclick、onerror、hrefjavascript:这些属性值直接剥除或转义。第三层对特殊字符做补充处理比如script、iframe、alert()这些高危关键词即使出现在纯文本里也要做转义替换。单纯用HtmlUtils.htmlEscape()把所有和都替换掉会破坏Normal业务中的富文本内容尤其是编辑器的HTML源码会直接变成一串乱码。所以清洗规则必须可配置至少要把“是否允许富文本”做成开关或者对不同的参数名称做差异化处理。3. 核心代码实现从清洗工具到Filter入口3.1 先从字符串清洗工具开始清洗工具是整个功能的地基。我写过一个简化版本思路是用白名单方式过滤危险标签同时转义保留的标签属性。public class XssCleanUtil { private static final Pattern SCRIPT_TAG_PATTERN Pattern.compile(script[^]*.*?/script, Pattern.CASE_INSENSITIVE | Pattern.DOTALL); private static final Pattern JAVASCRIPT_URI_PATTERN Pattern.compile(\\s*javascript\\s*:, Pattern.CASE_INSENSITIVE); private static final Pattern VBSCRIPT_URI_PATTERN Pattern.compile(\\s*vbscript\\s*:, Pattern.CASE_INSENSITIVE); private static final SetString ALLOWED_TAGS new HashSet(Arrays.asList( br, p, span, div, a, img, b, strong, i, em, u, ul, ol, li, table, tr, td, th, tbody, thead, h1, h2, h3, h4, h5 )); public static String clean(String value) { if (StringUtils.isEmpty(value)) { return value; } String cleaned removeScriptTag(value); cleaned removeUnsafeAttribute(cleaned); cleaned escapeJavascriptUri(cleaned); return cleaned; } private static String removeScriptTag(String value) { return SCRIPT_TAG_PATTERN.matcher(value).replaceAll(); } private static String removeUnsafeAttribute(String value) { String lower value.toLowerCase(); if (lower.contains(onclick) || lower.contains(onerror) || lower.contains(onload) || lower.contains(onfocus) || lower.contains(ondblclick) || lower.contains(onblur) || lower.contains(onchange) || lower.contains(onsubmit) || lower.contains(onkeyup) || lower.contains(onkeydown) || lower.contains(onkeypress)) { return value.replaceAll((?i)\\son\\w\\s*\\s*(\[^\]*\|[^]*|[^\\s]), ); } return value; } private static String escapeJavascriptUri(String value) { value JAVASCRIPT_URI_PATTERN.matcher(value).replaceAll(); return VBSCRIPT_URI_PATTERN.matcher(value).replaceAll(); } }注意这不是一个完整的HTML净化器如果真的要做富文本白名单过滤或者要符合比较严格的安全审计要求建议直接用专门的HTML Sanitizer库。但这个工具类适合作为自定义Filter的基础特别是老项目不方便引入重依赖的时候可以先跑起来再逐步加强规则。一定要记住清洗规则不能无限叠加否则代码会越来越难维护而且每次加强规则都会影响线上数据格式。最好把规则做成配置项按模块、按Controller路径、按参数名进行差异化放行。3.2 可清洗参数的请求包装器核心原理接下来是重头戏。我们需要继承HttpServletRequestWrapper并重写关键方法。先看看普通请求的包装器怎么写public class XssRequestWrapper extends HttpServletRequestWrapper { public XssRequestWrapper(HttpServletRequest request) { super(request); } Override public String getParameter(String name) { String value super.getParameter(name); return XssCleanUtil.clean(value); } Override public String[] getParameterValues(String name) { String[] values super.getParameterValues(name); if (values null) { return null; } String[] cleanedValues new String[values.length]; for (int i 0; i values.length; i) { cleanedValues[i] XssCleanUtil.clean(values[i]); } return cleanedValues; } Override public MapString, String[] getParameterMap() { MapString, String[] parameterMap super.getParameterMap(); MapString, String[] cleanedMap new HashMap(parameterMap.size()); for (Map.EntryString, String[] entry : parameterMap.entrySet()) { String[] values entry.getValue(); String[] cleanedValues new String[values.length]; for (int i 0; i values.length; i) { cleanedValues[i] XssCleanUtil.clean(values[i]); } cleanedMap.put(entry.getKey(), cleanedValues); } return cleanedMap; } Override public String getHeader(String name) { String value super.getHeader(name); return XssCleanUtil.clean(value); } }这里有个细节要说明getParameterMap()返回的Map在Servlet规范中可能是不可变的。实际使用中容器返回的往往是只读Map如果你直接修改里面的数组元素可能抛UnsupportedOperationException或者影响其他线程。所以我特意把每个数组都复制了一遍生成的cleanedMap也是新Map这样既能保证安全也避免了和容器内部数据结构产生耦合。另外getHeader()也可以按需重写。只要Header里带上了攻击代码也可以在请求进入Controller之前清洗掉。但如果有些Header值要参与sso签名验签之类的逻辑那就要小心别把所有Header都改了否则可能验签失败。我通常只清洗Referer、User-Agent这些不参与签名的字段但也不建议对Header做太激进的清洗因为你无法预知后续依赖哪些原始值。3.3 multipart请求专用包装器不能按普通InputStream方案走现在讲最关键的multipart。很多人在这个位置踩坑我自己也在这翻过车。一个常见的做法是在包装器里重写getInputStream()把请求体缓存到内存再提供重复读取能力。这种方式对application/x-www-form-urlencoded和application/json都没问题但到了multipart请求你一读InputStream就完了因为SpringMVC的MultipartResolver也需要从请求流中解析文件内容。两边抢同一个数据源轻则文件解析为空重则请求直接抛异常。所以在multipart请求上我的策略是“绕开请求体只清洗已解析的参数和Header”。容器在处理multipart请求时只要你调用了getParameter()或getParameterMap()Servlet 3.0以上版本的容器通常会把multipart请求里的文本字段和文件元数据解析出来放到ParameterMap里供你获取。虽然这是容器实现层面的行为但Tomcat、Jetty这些主流的Servlet容器至少都能拿到文本字段。我们要做的就是在getParameter()、getParameterValues()、getParameterMap()这几个方法上统一清洗。不能直接照抄普通包装器的原因很清楚如果你为了清洗字段先调用super.getParameter()容器内部会去解析multipart请求体。这是允许的SpringMVC后续解析时一般不会发生严重冲突因为容器已经把解析结果缓存住了。但如果你的包装器还重写了getInputStream()并缓存了原始流那就意味着MultipartResolver后面通过request.getInputStream()读文件时会读取到你缓存的那个流一旦顺序错乱文件内容就会缺失。所以我的multipart包装器是简化版只要处理参数清洗和Header清洗不碰InputStream和Readerpublic class MultipartXssRequestWrapper extends HttpServletRequestWrapper { private MapString, String[] cleanedParameterMap; public MultipartXssRequestWrapper(HttpServletRequest request) { super(request); } Override public String getParameter(String name) { String value super.getParameter(name); return XssCleanUtil.clean(value); } Override public String[] getParameterValues(String name) { String[] values super.getParameterValues(name); if (values null) { return null; } String[] cleanedValues new String[values.length]; for (int i 0; i values.length; i) { cleanedValues[i] XssCleanUtil.clean(values[i]); } return cleanedValues; } Override public MapString, String[] getParameterMap() { if (cleanedParameterMap null) { MapString, String[] parameterMap super.getParameterMap(); MapString, String[] cleanedMap new HashMap(parameterMap.size()); for (Map.EntryString, String[] entry : parameterMap.entrySet()) { String[] values entry.getValue(); if (values null) { cleanedMap.put(entry.getKey(), null); continue; } String[] cleanedValues new String[values.length]; for (int i 0; i values.length; i) { cleanedValues[i] XssCleanUtil.clean(values[i]); } cleanedMap.put(entry.getKey(), cleanedValues); } cleanedParameterMap cleanedMap; } return cleanedParameterMap; } Override public String getHeader(String name) { String value super.getHeader(name); return XssCleanUtil.clean(value); } }加缓存cleanedParameterMap是很有必要的因为getParameterMap()和getParameter()会在一次请求中被调用多次每次都重新遍历再清洗性能损耗会翻倍。首次访问时计算一次之后直接返回缓存Map即可。3.4 Filter入口识别请求类型并路由到正确的包装器现在写Filter本尊关键是判断Content-Type。如果请求是multipart/form-data就走multipart专用包装器否则走普通包装器。为了避免普通包装器后续处理JSON请求体时无法重复读取我也在普通包装器里加入缓存流逻辑这里一并处理。WebFilter(filterName xssFilter, urlPatterns /*) Order(1) public class XssFilter implements Filter { private static final String CONTENT_TYPE_MULTIPART multipart/form-data; Override public void init(FilterConfig filterConfig) { // 可以在这里读取初始化参数比如排除路径、白名单配置 } Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) servletRequest; String contentType request.getContentType(); if (contentType ! null contentType.toLowerCase().startsWith(CONTENT_TYPE_MULTIPART)) { chain.doFilter(new MultipartXssRequestWrapper(request), servletResponse); } else { chain.doFilter(new XssRequestWrapper(request), servletResponse); } } Override public void destroy() { // 资源释放 } }如果你使用Spring Boot加WebFilter后还要在启动类上配合ServletComponentScan或者在配置类里用FilterRegistrationBean注册。如果是传统SpringMVC项目就在web.xml里配置filter和filter-mapping。建议把过滤器顺序放在最前面至少要在Spring的CharacterEncodingFilter之后、其他业务过滤器之前。因为编码不对的话中文参数清洗后会产生乱码。3.5 这里有一个取舍JSON请求体怎么处理很多SpringMVC项目现在不用表单提交而是用application/json。JSON数据不能通过getParameter()读取只能在getInputStream()里读取原始JSON字符串再解析。所以在处理JSON请求时普通包装器的流缓存能力就派上用场了。常规做法是在包装器里把输入流缓存下来然后重写getInputStream()和getReader()返回缓存的数据。这样SpringMVC的RequestBody在反序列化时拿到的其实是清洗后的JSON字符串。public class CacheableXssRequestWrapper extends XssRequestWrapper { private byte[] cachedBody; public CacheableXssRequestWrapper(HttpServletRequest request) throws IOException { super(request); this.cachedBody request.getInputStream().readAllBytes(); } Override public ServletInputStream getInputStream() throws IOException { ByteArrayInputStream byteArrayInputStream new ByteArrayInputStream(cachedBody); return new ServletInputStream() { Override public int read() { return byteArrayInputStream.read(); } Override public boolean isFinished() { return byteArrayInputStream.available() 0; } Override public boolean isReady() { return true; } Override public void setReadListener(ReadListener listener) { // 根据需求实现 } }; } Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8)); } }但你要注意一个安全问题JSON字符串里的字段名和字段值混在一起直接对整个字符串做字符串替换极有可能把字段名也改掉。比如{alert:x}你可能会写成{x:x}这样Jackson反序列化就会失败。所以在JSON清洗这里严谨的做法是先拿到JSON字符串用Jackson或Fastjson解析成对象然后递归清洗所有String字段再序列化回JSON字符串。如果项目里有特殊类型比如字段值是数字或布尔值保持不变即可。考虑到篇幅我这里只给一个可行思路JSON清洗最好不要在Filter里做完全的序列化和反序列化否则会引入额外的性能开销还可能破坏数据类型。更稳妥的方案是分两层Filter负责参数、Header、表单通用清洗ControllerAdvice里用RequestBodyAdvice去做JSON字段的精准清洗。如果非要统一在Filter层做一定要做好对象序列化的稳定性和容错。4. 配置注册与过滤范围控制4.1 传统SpringMVC的web.xml配置方式在传统SpringMVC项目里web.xml配置如下filter filter-namexssFilter/filter-name filter-classcom.demo.filter.XssFilter/filter-class init-param param-nameexcludes/param-name param-value/api/notify, /health/check/param-value /init-param /filter filter-mapping filter-namexssFilter/filter-name url-pattern/*/url-pattern dispatcherREQUEST/dispatcher /filter-mappingurl-pattern用/*能覆盖大部分请求。但要注意dispatcher如果只配REQUEST那么转发请求和异步请求不会经过过滤器。如果项目里有RequestDispatcher.forward()等内部转发可以再加一行dispatcherFORWARD/dispatcher配置。4.2 Spring Boot中的注册方式如果是Spring Boot项目我一般直接用FilterRegistrationBean注册方便设置顺序和过滤路径Configuration public class FilterConfig { Bean public FilterRegistrationBeanXssFilter xssFilterRegistration() { FilterRegistrationBeanXssFilter registration new FilterRegistrationBean(); registration.setFilter(new XssFilter()); registration.addUrlPatterns(/*); registration.setOrder(1); registration.addInitParameter(excludes, /api/notify, /health/check); return registration; } }这里setOrder(1)是最高优先级尽量排在Spring的CharacterEncodingFilter之后否则可能出现编码问题。如果项目里还有权限过滤器、授权过滤器XSS过滤器应该放在权限校验之前还是之后我建议放在最外层因为XSS过滤不依赖用户身份越早清洗后续过滤器拿到的数据就越安全。4.3 排除某些特殊路径不是所有请求都需要XSS过滤。比如提供给第三方API回调的接口数据格式有严格约定你不能轻易改内容否则验签或者解析会失败。再比如富文本编辑器图片上传接口文件上传的二进制内容不能被清洗。我一般会在Filter里加一个排除列表用逗号分隔路径。private SetString excludes new HashSet(); private void initExcludes(FilterConfig filterConfig) { String excludesParam filterConfig.getInitParameter(excludes); if (StringUtils.hasText(excludesParam)) { String[] paths excludesParam.split(,); for (String path : paths) { excludes.add(path.trim()); } } } private boolean isExcluded(String requestUri) { for (String excludePath : excludes) { if (requestUri.startsWith(excludePath)) { return true; } } return false; }在doFilter开头检查String requestUri request.getRequestURI(); if (isExcluded(requestUri)) { chain.doFilter(request, response); return; }排除路径可以做startsWith匹配不要只做绝对相等这样灵活性更高。否则/api/notify/abc这种子路径会漏出去。5. 实战中的坑点、问题排查与性能优化5.1 问题一multipart请求中的普通字段为什么没有经过过滤很多人第一次集成后会遇到这个问题用Postman测试application/x-www-form-urlencoded时XSS过滤生效了换成multipart/form-data后明明Filter也走了但Controller里拿到的字段还是没有清洗。排查步骤很清晰第一步确认过滤器真的拦截到了请求。可以在doFilter里打日志打印Content-Type和RequestURI。第二步确认包装器被正确创建。如果Content-Type判断条件写错了比如没有处理大小写或者请求带的Content-Type是multipart/related、multipart/mixed很可能走错了分支。第三步确认Controller里拿参数的途径。如果是用RequestParam获取的普通字段那getParameter()会被调用包装器会生效。如果是用MultipartFile直接取文件那自然过滤不了文件内容也不需要过滤。第四步确认你用的Servlet版本。老版本的Servlet容器不一定会主动解析multipart请求里的普通字段需要你调用request.getParameter()前先让Spring的MultipartResolver完成解析。如果顺序不对拿到的ParameterMap可能是空的。5.2 问题二缓存InputStream导致MultipartFile解析为空这是最典型的一个翻车现场。我在给别人review代码时就见过这种写法统一包装器里重写了getInputStream()把原始流缓存起来给后续所有读取使用。结果文件上传功能挂了上传后的文件永远是0字节或者直接报Required request part file is not present错误。原因并不复杂SpringMVC在处理multipart时内部通过request.getParts()去解析请求体需要读取原始的输入流。但你的包装器把请求流读出来后存入到了缓存Spring再想读的时候原始流已经到头了。解决方式就是我前面提到的multipart请求不要重写getInputStream()和getReader()只做参数清洗就够了。非multipart的JSON请求才需要缓存流而且这种缓存也要判断Content-Type避免无谓地消耗内存。5.3 问题三清洗逻辑误杀正常业务数据还有一类高频问题不是漏洞没堵上而是堵过头了。业务数据里的正常内容被误伤比如产品描述里写了“请点击 这里 ”结果标签被剥掉了或者评论里提到“javascript”这个词被escapeJavascriptUri把冒号后面内容全替换了。如果清洗规则写得过于粗暴用全局替换的方式把a、img等标签一律去掉富文本功能就废了。所以我强烈建议做两个事情第一清洗规则分级。最简单的级别是转义全部HTML字符适合纯文本输入中级是删除高危标签但保留基础格式标签高级是完整解析HTML标签属性做白名单过滤。第二增加参数名维度的判断。比如content字段允许富文本就用白名单模式username、nickname这些不允许任何HTML就用转义模式。不要把同一个规则用到所有字段上。5.4 问题四性能损耗与内存占用XSS过滤器本质上是把“写入”操作变成了“清洗写入”所以性能上肯定会有损耗。做了缓存流之后每个请求都要把请求体先读一遍到byte数组如果请求体是几十MB的大文件这个内存开销会非常恐怖。我的建议是先判断请求体大小在包装器里只缓存一定大小范围内的数据。超过阈值就直接放行不缓存不清洗。因为大文件上传一般对应的是图片、音频、视频这类二进制资源XSS攻击面不在文件内容本身。就算上传了一个包含恶意代码的HTML文件那也是下载后由浏览器执行时才构成风险与请求参数的清洗是两码事。实际项目里我会配置一个MAX_CACHE_SIZE默认2MB。超过2MB的请求不读缓存只做参数和Header清洗这样能有效防止被Content-Length很大的恶意请求拖垮内存。5.5 问题五请求参数二次编码导致的绕过还有一个必须注意的问题攻击者经常把script变成%3Cscript%3E来绕过过滤。如果你的过滤器只过滤了解码后的值没注意URL本身的编码形式那么在参数进入Controller时有些容器会先做URL解码有些不会最后清洗流程就有可能被跳过。常规做法有两种一种是在getParameter()返回前先做URL解码再清洗然后再编码回去。但这样容易破坏数据原本的格式不建议。更稳妥的方式是在Filter里额外检查getQueryString()对原始查询字符串做一次清洗再在参数取值层做一次清洗。双层过滤至少能让大多数编码绕过失效。5.6 性能优化建议我在一个访问量比较大的SpringMVC服务里做压测时发现XSS过滤器的性能瓶颈主要集中在正则匹配上。尤其是多个高消耗正则串行校验一个超长字符串时CPU占用率会明显上升。优化方向有三个精简正则不追求大而全关键把script、javascript:、事件属性、iframe、object、embed这几类高危险特征识别出来就行。增加短路机制如果字符串里完全没有、、空格、引号这些敏感字符直接返回原值跳过后续所有正则。对重复访问的getParameter()结果做缓存避免同一个参数在一次请求里被清洗多次。6. 实测总结与经验建议整个方案上线后我大概统计了一下相同时间窗口内XSS漏洞扫描数量下降了九成以上。剩余问题基本出现在富文本编辑器内容和JSON嵌套字段这两个极端场景而这两块也不是一个Filter能彻底解决的需要做前端过滤、后端白名单、输出编码三道防线同时发力。最后分享几个个人经验。如果你负责的是老项目先别急着把所有Controller接入过滤器最好先在测试环境把全链路接口跑一遍。重点看三类接口文件上传、富文本编辑、第三方回调。这些接口最容易因为清洗规则而产生副作用。如果你是在Spring Boot项目里用尽量用FilterRegistrationBean方式注册因为你会在里面设置Order和初始化参数比单纯使用WebFilter更好排查问题。不要迷信一把梭的过滤器。XSS防护应该是分层的Filter负责挡掉大部分反射型XSS和存储型XSSController在关键位置配合参数校验前端做输出编码和CSP数据层、渲染层也要做好输出转义。Filter能做的是把攻击面缩小但不可能一夫当关把所有风险全部堵死。如果你现在正准备给自己负责的SpringMVC项目加XSS过滤器我建议从普通表单请求开始跑通再加入JSON请求体清洗最后再单独花一下午时间去处理multipart请求。这个顺序能让你少走很多弯路也能把每个环节的边界看得更清楚。