先说一个我前阵子遇到的真实案例。我们某个管理后台的“个人备注”功能被人塞了一行scriptalert(document.cookie)/script当时没有造成实际损失但这段代码在整个查询页面回显同事一打开列表浏览器直接弹窗。查到最后问题不在前端不在网关而是后端接口把用户输入原封不动地写进了数据库。存储型 XSS 难防就难在“写入时无人感知读取时全面爆发”。只要有人绕过前端直接打接口或者前端校验逻辑被改脏数据就能成功入库。后面我在团队里落地了一套纯后端轻量方案一个注解XssClean标记需要清洗的接口一个XssFilter类统一切入针对前台传入的普通参数、请求头、JSON Body 做清洗和拦截不依赖任何第三方安全框架整套代码量可控适合大多数中小团队直接参考。1. 为什么存储型 XSS 必须靠后端兜底1.1 攻击路径与危害面存储型 XSS 的攻击流程可以概括为三步攻击者把恶意脚本作为正常内容提交给服务端服务端将其持久化到数据库其他用户请求某个页面时服务端把这段内容拼进 HTML 响应返回浏览器解析并执行了脚本攻击者达成目的。和反射型 XSS 相比存储型的危害面更大。反射型是一次性的攻击者必须诱导受害者点击特定链接存储型只要脚本成功入库后续所有访问对应页面的人都会中招。它直接威胁到 cookie、会话令牌、页面内容完整性甚至可以配合钓鱼页面做二次攻击。常见出现场景包括昵称、签名、留言、评论、问卷调查结果、个人简介等需要用户填写文本的地方。我在项目里排查时还发现一个现实问题恶意数据一旦写入数据库光靠改代码是救不了存量数据的历史脏数据还躺在库里需要写脚本清洗或者人工处理。这也是我后来坚持在入口做统一拦截的原因——防住源头比事后擦屁股便宜得多。1.2 前端校验为什么拦不住前端加校验比如限制输入长度、过滤特殊字符、正则匹配本质上只是对人机交互的一种约束不是协议层的约束。我平时调试接口经常直接撸一段 curl 报文往服务端打页面里的v-model规则、onblur校验、正则表达式一个都不会执行。换句话说只要后端对外暴露了 HTTP 接口攻击者随时可以构造原始报文绕过前端。接口联调阶段团队内部为了传参方便也常常跳过前端页面直接请求后端。如果后端接口默认信任“前端已经过滤过”那就等于把所有入口都敞开给攻击者。安全领域有一句话值得反复强调一切用户输入都不可信。这个“输入”不仅指表单字段还包括请求头、Cookie、上传文件名甚至路径参数。纯后端拦截的意义就在这里它不看前端脸色在数据进入业务逻辑之前就把危险内容处理掉。1.3 从成本看方案取舍当时团队里有人提出引入现成的安全过滤器也有人建议直接用 Owasp 的 HTML Sanitizer 或 Jsoup 做白名单清洗。这些方案功能强大但对一个以普通接口为主、没有富文本编辑需求的项目来说引入成本和维护成本都不低。我更倾向于做一个“最小可用”的组件不改造现有接口不打扰没有标注的请求用一个注解加上一个类就能说清楚逻辑。新同学接手时看一眼注解就知道哪些接口做了防护不用去翻几百行配置。这个思路也是这篇文章的核心先讲清楚为什么这么设计再给完整代码。2. 方案推演Filter、拦截器、AOP 到底选哪个2.1 Spring Boot 请求链路里它们分别在哪一步Spring Boot 处理一个 HTTP 请求的完整链路大致是Servlet Filter 最先执行然后进入 DispatcherServlet再经过 HandlerInterceptor最后通过 HandlerAdapter 调用 Controller 方法。AOP 拦截则发生在目标方法调用前后是最接近业务代码的一层。如果要在请求进入 Controller 之前统一清洗入参最简单的想法当然是越早越好。Filter 处于链路最前端此时 request 还没有被解析成 Controller 的入参Interceptor 距离 Controller 更近但参数绑定流程还没开始AOP 则已经拿到方法签名和参数值了。这个链路顺序直接决定了工具选型。用 Filter 可以在参数绑定之前把 request 对象替换成包装器让后续所有解析逻辑都读到清洗后的数据用 Interceptor 则只能做“检查”做不了“替换”用 AOP 则需要在同一套切面里处理不同接口的复杂参数结构。2.2 HandlerInterceptor 的坑包装不了 request我第一次做这个需求时很自然地想用 HandlerInterceptor。理由也很直接拦截器能拿到 HandlerMethod可以直接读取方法上的注解语义非常清晰。但实现到一半就踩了坑。preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)里虽然可以拿到 request 对象但这个对象是 DispatcherServlet 传入的。你在方法里写request new XssWrapper(request)只是把局部变量重新赋值了DispatcherServlet 后续做参数解析时用的仍然是原始 request。Spring MVC 的数据绑定发生在 HandlerAdapter 阶段它不会回头去找拦截器里生成的新对象。这一点非常关键也是很多文章没点破的坑。拦截器适合做登录校验、权限校验、请求日志这类“看一眼就放行”的事情不适合做“把请求对象换掉再传给下游”的事情。真要替换 request必须借助 Servlet 层面的 Filter。2.3 最终方案注解驱动的过滤器我的方案是结合两者优点使用OncePerRequestFilter做真正的清洗在doFilterInternal里把 request 包装后通过filterChain.doFilter(wrapper, response)传下去后续整个链路拿到的都是清洗后的参数来源。注解的读取则靠注入RequestMappingHandlerMapping在过滤器中动态查询当前请求对应的 HandlerMethod。如果方法或类上标注了XssClean就进入清洗流程没有标注的接口完全不受影响。这个组合拿到了注解的语义控制能力又保留了 Filter 早期介入的能力。回调一下文章标题说的“一个类加一个注解结束”实际交付物就是这两个东西一个负责声明哪些接口需要清洗的注解一个负责清洗的过滤器类。注解不只是开关还能指定清洗模式和排除字段后面我会详细说明。3. 核心代码一个注解加一个类实现全量清洗3.1 注解定义与参数设计import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; Target({ElementType.TYPE, ElementType.METHOD}) Retention(RetentionPolicy.RUNTIME) Documented public interface XssClean { boolean enabled() default true; XssType type() default XssType.STRICT; String[] exclude() default {}; enum XssType { STRICT, LOOSE } }三个字段的设计理由如下。enabled是为了让你在类上统一开开关。比如一个 Controller 类整体标注了XssClean其中某个接口不需要清洗可以在方法上写XssClean(enabled false)局部关掉。type区分严格模式和宽松模式。严格模式对输入做 HTML 实体转义把script变成lt;scriptgt;适合昵称、备注等纯文本字段宽松模式只剥离 script、iframe 标签、事件属性和危险协议保留基础格式适合需要保留简单排版的内容。exclude用于指定不处理哪些字段。比如富文本字段希望原样保留给编辑器可以在注解里把字段名列出来比如exclude {content}。3.2 Filter 主体如何识别注解并决定是否过滤import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.method.HandlerMethod; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; Component public class XssFilter extends OncePerRequestFilter { private static final String XSS_CONFIG_KEY XSS_CLEAN_CONFIG; private final RequestMappingHandlerMapping handlerMapping; public XssFilter(RequestMappingHandlerMapping handlerMapping) { this.handlerMapping handlerMapping; } Override protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { XssClean config resolveConfig(request); if (config null || !config.enabled()) { return true; } request.setAttribute(XSS_CONFIG_KEY, config); return false; } Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { XssClean config (XssClean) request.getAttribute(XSS_CONFIG_KEY); XssHttpServletRequestWrapper wrapper new XssHttpServletRequestWrapper(request, config); filterChain.doFilter(wrapper, response); } private XssClean resolveConfig(HttpServletRequest request) { try { HandlerExecutionChain chain handlerMapping.getHandler(request); if (chain null) { return null; } Object handler chain.getHandler(); if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod (HandlerMethod) handler; XssClean methodAnno handlerMethod.getMethodAnnotation(XssClean.class); if (methodAnno ! null) { return methodAnno; } return handlerMethod.getBeanType().getAnnotation(XssClean.class); } } catch (Exception e) { // 静态资源、非标准 HandlerMethod 等场景直接放行 } return null; } }shouldNotFilter是OncePerRequestFilter提供的扩展点返回 true 表示跳过整个过滤器。这里我利用它做了两件事查出当前请求对应的注解决定是否过滤把注解实例存到 request attribute 里避免doFilterInternal里再查一遍。RequestMappingHandlerMapping.getHandler(request)可能返回 null也可能在处理静态资源时抛异常所以必须包一层 try-catch失败就放行。一个请求只会执行一次OncePerRequestFilter所以 config 存 attribute 是安全的。3.3 请求包装器参数、请求头、请求体全覆盖import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import org.springframework.util.StreamUtils; public static class XssHttpServletRequestWrapper extends HttpServletRequestWrapper { private final XssClean config; private byte[] rawBody; private byte[] cleanedBody; public XssHttpServletRequestWrapper(HttpServletRequest request, XssClean config) { super(request); this.config config; } Override public String getParameter(String name) { String value super.getParameter(name); return isExcluded(name) ? value : XssFilter.cleanText(value, config.type()); } Override public String[] getParameterValues(String name) { String[] values super.getParameterValues(name); if (values null) { return null; } if (isExcluded(name)) { return values; } String[] cleaned new String[values.length]; for (int i 0; i values.length; i) { cleaned[i] XssFilter.cleanText(values[i], config.type()); } return cleaned; } Override public MapString, String[] getParameterMap() { MapString, String[] source super.getParameterMap(); if (source null || source.isEmpty()) { return source; } MapString, String[] target new HashMap(source.size()); for (Map.EntryString, String[] entry : source.entrySet()) { String key entry.getKey(); String[] value entry.getValue(); if (isExcluded(key) || value null) { target.put(key, value); continue; } String[] cleanedValue new String[value.length]; for (int i 0; i value.length; i) { cleanedValue[i] XssFilter.cleanText(value[i], config.type()); } target.put(key, cleanedValue); } return target; } Override public String getHeader(String name) { String value super.getHeader(name); return isExcluded(name) ? value : XssFilter.cleanText(value, config.type()); } Override public ServletInputStream getInputStream() throws IOException { if (rawBody null) { byte[] original StreamUtils.copyToByteArray(super.getInputStream()); this.rawBody original; this.cleanedBody XssFilter.cleanBody(original, config); } return new ServletInputStream() { private final ByteArrayInputStream in new ByteArrayInputStream(cleanedBody); Override public boolean isFinished() { return in.available() 0; } Override public boolean isReady() { return true; } Override public void setReadListener(ReadListener readListener) { // 简单实现不走异步读取保持同步读取即可 } Override public int read() { return in.read(); } }; } Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8)); } private boolean isExcluded(String name) { if (name null || config.exclude().length 0) { return false; } return Arrays.asList(config.exclude()).contains(name); } }这段代码覆盖了三种最常见的输入入口表单参数走getParameter系列方法请求头走getHeader请求体走getInputStream。有个细节要提醒getInputStream和getReader只能调用一次随意重写容易导致后续解析器拿不到数据。这里我把原始 body 缓存到字节数组再基于清洗后的字节数组构造新的ServletInputStream这样即使被多次读取也不会出问题。如果你担心清洗请求头会影响 token、签名之类的信息可以把getHeader那段删掉或者只保留对自定义业务头生效的逻辑。大多数项目里 header 不会回显到页面这段代码属于锦上添花。3.4 JSON Body 清洗逐字段递归处理public static byte[] cleanBody(byte[] body, XssClean config) { if (body null || body.length 0) { return body; } String text new String(body, StandardCharsets.UTF_8); String cleaned; try { ObjectMapper mapper new ObjectMapper(); JsonNode root mapper.readTree(text); JsonNode result cleanNode(root, config); cleaned mapper.writeValueAsString(result); } catch (Exception e) { // 不是合法 JSON 时按普通文本处理 cleaned cleanText(text, config.type()); } return cleaned.getBytes(StandardCharsets.UTF_8); } private static JsonNode cleanNode(JsonNode root, XssClean config) { if (root null) { return null; } if (root.isObject()) { ObjectNode objectNode (ObjectNode) root; ListString fieldNames new ArrayListString(); root.fieldNames().forEachRemaining(field - fieldNames.add(field)); for (String field : fieldNames) { JsonNode child objectNode.get(field); if (child.isTextual() !isFieldExcluded(field, config)) { objectNode.put(field, cleanText(child.asText(), config.type())); } else if (child.isContainerNode()) { cleanNode(child, config); } } return objectNode; } if (root.isArray()) { ArrayNode arrayNode (ArrayNode) root; ListJsonNode items new ArrayListJsonNode(); arrayNode.forEach(item - items.add(item)); arrayNode.removeAll(); for (JsonNode item : items) { arrayNode.add(cleanNode(item, config)); } return arrayNode; } return root; }为什么不能对整个 JSON 字符串直接做 HTML 转义因为 JSON 结构中的双引号、反斜杠一旦被转义JSON 解析直接失败。逐字段递归处理可以保证每个 value 被清洗同时保留 JSON 结构完整还方便按字段名排除。这里用到了 Jackson 的ObjectMapper因为 Spring Boot 项目默认已经依赖 Jackson不需要额外引入。当前isFieldExcluded只对顶层字段名生效如果你的业务有嵌套字段需要排除可以把字段路径拼进递归参数里比如user.nickname这样带点号的路径实现思路是一样的。3.5 清洗工具实现与正则策略private static final Pattern SCRIPT_TAG_PATTERN Pattern.compile((?is)\\s*/?\\s*(script|iframe|object|embed)[^]*.*?/\\s*\\1\\s*|\\s*/?\\s*(script|iframe|object|embed)[^]*); private static final Pattern EVENT_ATTR_PATTERN Pattern.compile((?is)\\son[a-z]{2,}\\s*\\s*(\[^\]*\|[^]*|[^\\s])); private static final Pattern DANGEROUS_PROTOCOL_PATTERN Pattern.compile((?i)\\s*(javascript|vbscript|data)\\s*:); public static String cleanText(String input, XssClean.XssType type) { if (input null || input.isEmpty()) { return input; } if (type XssClean.XssType.LOOSE) { String cleaned input; cleaned SCRIPT_TAG_PATTERN.matcher(cleaned).replaceAll(); cleaned EVENT_ATTR_PATTERN.matcher(cleaned).replaceAll(); cleaned DANGEROUS_PROTOCOL_PATTERN.matcher(cleaned).replaceAll($1); return cleaned; } StringBuilder sb new StringBuilder(input.length()); for (int i 0; i input.length(); i) { char c input.charAt(i); switch (c) { case : sb.append(amp;); break; case : sb.append(lt;); break; case : sb.append(gt;); break; case : sb.append(quot;); break; case \: sb.append(#39;); break; default: sb.append(c); } } return sb.toString(); }严格模式直接做实体转义简单粗暴且有效。宽松模式用正则剥离危险内容脚本标签、iframe、object、embed、事件属性、危险协议都在处理范围内。这套正则不是完美的比如嵌套变体和编码变体就无法保证全部拦截。想做更彻底的方案需要换成白名单引擎或循环解码校验后面我会专门分析边界。4. 清洗策略与绕过分析4.1 三种策略到底怎么选方案处理方式适用场景主要风险STRICT 严格模式对 做实体转义昵称、评论、备注、搜索词富文本会被转成纯文本LOOSE 宽松模式移除标签、事件属性、危险协议简单富文本、含样式的业务字段正则永远追不上所有变体Jsoup 白名单只放行指定标签和属性复杂富文本、文章内容依赖较重需要维护白名单大多数接口用 STRICT 就够了。比如昵称存lt;scriptgt;alert(1)lt;/scriptgt;前端用文本方式展示时用户看到的就是scriptalert(1)/script这几个字符不会执行。LOOSE 适合你对样式有一定要求但不能接受脚本的场景。比如允许用户填b注意/b但不允许img srcx onerroralert(1)。这种模式牺牲了一定的安全性来保留展示效果需要配合测试用例不断补充正则。如果你的项目有完整富文本编辑器比如用户自己上传文章、粘贴来自网页的内容那单靠过滤器的正则是扛不住的。老老实实引入白名单清洗引擎让开发同学维护一份允许的标签和属性列表这是最稳妥的方式。4.2 常见绕过姿势与应对思路XSS 的绕过方式千奇百怪最常见的有这么几类。大小写混合比如ScRiPt。应对方式就是正则加CASE_INSENSITIVE我在上面的正则里已经加了。事件属性注入比如img srcx onerroralert(1)。这种利用的是 HTML 标签属性弱口令应对方式是剥离所有on[a-z]形式的事件属性我的 EVENT_ATTR_PATTERN 就是干这个的。危险协议构造比如hrefjavascript:alert(1)。应对方式是识别javascript:、data:、vbscript:这类协议头并做替换。还有一种情况是编码变体比如攻击者在 URL 里传%3Cscript%3EServlet 容器在getParameter阶段已经做了一次 URL 解码所以过滤器在清洗时拿到的还是script能正常拦截。如果遇到二次编码、第三方网关再编码的情况就需要在你的过滤链前再加一道解码规则。4.3 这套方案不适用哪些场景不是所有接口都适合无脑加XssClean。接口需要接收代码片段、Markdown 原文、JSON 模板等特殊内容的场景清洗可能会破坏数据结构导致业务出错。这类接口建议在注解上设置enabled false或者不加注解单独在业务层处理。SQL 拼接场景也别指望这个过滤器。它只处理 XSS 相关的脚本注入不处理 SQL 注入。SQL 注入请老老实实用参数绑定这不是一个过滤器能包办的事。同理文件上传、目录穿越、越权访问这些问题也需要在各自层面单独防护。我心里一直有个底线这个过滤器是入口防线之一不是全部安全方案。接口设计上坚持“输入不可信、输出不裸奔”比任何一个过滤器都重要。5. 在项目里 30 分钟接完的实操步骤5.1 接入步骤清单第一步把注解类XssClean放到项目的 common 包或者安全相关包下。第二步把过滤器类XssFilter放到能被 Spring Boot 扫描到的包路径下。类上有Component启动后自动注册。第三步在需要防 XSS 的 Controller 方法或 Controller 类上添加XssClean。第四步启动项目用 curl 或者 Postman 发一条带恶意脚本的请求验证效果。整个过程不用改配置文件不用设置 filter-mapping不用动了不起的全局拦截链条。5.2 Controller 示例与两种提交方式验证假设有一个保存用户信息的接口RestController RequestMapping(/api/user) public class UserController { PostMapping(/save) XssClean(type XssClean.XssType.STRICT) public Result save(RequestBody UserDTO user) { userService.save(user); return Result.ok(); } PostMapping(/save-form) XssClean(type XssClean.XssType.STRICT) public Result saveForm(UserDTO user) { userService.save(user); return Result.ok(); } }用 JSON 方式提交curl -X POST http://localhost:8080/api/user/save \ -H Content-Type: application/json \ -d {nickname:scriptalert(1)/script,bio:bhi/b}如果你在接口里把收到的nickname打印出来会是{nickname:lt;scriptgt;alert(1)lt;/scriptgt;,bio:lt;bgt;hilt;/bgt;}用表单方式提交curl -X POST http://localhost:8080/api/user/save-form \ -d nicknamescriptalert(1)/script同样能清洗成功。两条链路的区别是表单参数走的是getParameter系列JSON body 走的是getInputStream加 Jackson 递归清洗。5.3 注解的三种典型用法类上统一开XssClean(type XssClean.XssType.STRICT) RestController RequestMapping(/api/user) public class UserController { }这样类下的所有接口都会被清洗。如果你想类里某个接口不开XssClean(enabled false) PostMapping(/raw) public Result raw(RequestBody RawDTO dto) { }这种局部关闭的写法适合类似文章转存储这类需要保留原始内容的场景。排除种子字段XssClean(type XssClean.XssType.LOOSE, exclude {content}) PostMapping(/article) public Result saveArticle(RequestBody ArticleDTO dto) { }这里的content字段会原样进入业务层其他字段走宽松清洗。6. 踩坑记录与快速排查6.1 拦截器里包装 request 的误区我最早想在 HandlerInterceptor 里实现写完发现 Controller 拿到的还是原始参数。原理前面已经说过但这里值得再强调一次。preHandle方法里的 request 参数只是局部引用你替换它不会影响 DispatcherServlet 后续的数据绑定。这个坑还有个变体有人把新包装器塞进RequestContextHolder以为能骗过 Spring 的请求上下文。实测无效因为 Spring MVC 数据绑定是从 DispatcherServlet 的方法参数里取 request而不是从上下文里找。所以要做请求内容替换请认准 Filter 层。6.2 JSON 请求为什么没被清洗如果你只重写了getParameter很容易碰到一个测试结果表单请求清洗生效了JSON 请求完全没变化。原因很简单RequestBody的 JSON 解析走的是getInputStream根本不经过getParameter。很多人第一次写 XSS 过滤器就漏在这里。解决方案就是前面代码里的cleanBody把getInputStream重写掉读取原始 body、清洗、再输出。注意getInputStream和getReader只能读取一次必须把原始 body 缓存住否则 Controller 解析时可能读到空内容接口直接报 400。6.3 存储转义后前端乱码的坑有次联调前端反馈说昵称显示成了lt;scriptgt;而不是正常的文本内容。排查下来是后端存的是转义后的字符串模板引擎在渲染时又做了一次转义双重转义导致界面出现了乱码。这个问题本质上是团队约定问题。后端存转义后的安全字符串前端在文本插值场景不应该再做二次转义如果前端必须用 v-html 渲染富文本后端只能输出已经过白名单清洗的内容。上这套过滤器之前建议先跟前端同步清楚存储和渲染的策略不然联调时容易扯皮。6.4 上传接口被误伤的坑加了过滤器后有同事反馈文件上传接口失败看日志发现二进制内容在解析时被当成文本处理了。这个问题发生在cleanBody里上传接口的 body 是 multipart 格式包含二进制文件流直接用 UTF-8 转字符串再清洗很容易把文件内容搞坏。处理方式是先判断isMultipartContent(request)如果是 multipart 请求跳过 body 清洗只对普通表单参数做过滤。文件上传本身有独立的安全要求比如扩展名校验、大小限制、病毒扫描这些不应该和 XSS 清洗混在一起。6.5 快速排查速查表现象可能原因处理方式接口参数没被清洗注解没标上或 Filter 没被扫描检查类路径确认注解标注位置JSON 请求没被清洗只重写了 getParameter没处理 getInputStream参考 3.4 实现 body 清洗清洗后接口报 400对整串 JSON 做了转义导致 JSON 结构损坏改用逐字段递归清洗上传文件报错Filter 把二进制 body 当文本处理增加 multipart 判断跳过 body 清洗前端显示转义字符串存储已转义模板又转义一次与前端约定渲染策略未标注的接口也被过滤类上标注了 XssClean检查注解是标在类还是方法上我在实际使用中最大的体会是安全方案能不能落地往往不取决于技术深度而取决于接入成本和对既有接口的侵入程度。一个注解加一个类团队里后端同学看一次就会用不用在几十个 Controller 里做重复劳动。最后再分享一个小技巧先把这套过滤器接到测试环境上跑几天把业务接口的测试用例过一遍重点观察两个点——被exclude的字段是否真的原样保留以及前端展示有没有因为转义策略变化而出现异常。确认没问题之后再逐步推广到生产环境而不是上线当天一次性全量启用。这样能最大程度减少“过滤误伤”带来的线上事故。