ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

一串“9”为何能炸穿系统?边界值测试与输入校验实战解析

一串“9”为何能炸穿系统?边界值测试与输入校验实战解析 先问你一个问题如果你的用户注册接口收到一个字段叫 phone值是一串99999999999999你的第一反应是什么拉黑直接报错还是默默存库我之所以想写这篇东西是因为这串看起来像键盘上随便敲出来的 9曾经同时把我们的前端校验、后端参数校验、网关协议、日志系统挨个炸了一遍。那天的排障过程让我意识到很多团队对输入校验的理解还停留在“正则校验必填字段”这个层面却忽略了真正的边界值和脏数据会以什么样意想不到的方式穿过所有防线。这篇文章就围绕这种“不正常的 9”展开它到底是谁、经过系统时会触发什么问题、怎么设计才能拦得住也顺便整理几个我从真实工单里总结出来的排查技巧。如果你平时写接口、维护后台系统或者在做测试用例设计这篇内容可以直接对照着复用。这种“一串全 9”的输入本质上是一个极好的边界样本。它看着像垃圾但和随机乱码完全不一样——它是一串有规律、有代表性、能精准触发系统缺陷的测试数据。很多测试同学在写用例时会用“999999999999999”填手机号、用“99999999999”当验证码用“99999999999999”当金额这些都不是随手乱敲它们每一条都可能摸到某一层系统的边界。把这个输入当成一场小型专项测试去拆你会发现它能暴露的问题远远不止“格式校验没过”这么简单。1. 内容整体设计与思路拆解1.1 边界值思路比等价类更省力但更容易被人忽略软件测试里有个很经典的方法叫“等价类划分”核心思想是把输入分成若干类同一类里挑一个代表值测就可以了。比如手机号字段合法值是 11 位数字非法值是“非数字”“不足 11 位”“超过 11 位”这样设计用例确实省事。但等价类有一个盲区它只区分“合法”和“非法”没有专门去照顾“合法范围内最极端的那个值”和“非法范围里最靠近边界的那一个值”。而大多数线上事故恰恰发生在边界上不在典型的正常值和典型的乱码上。“边界值分析”就是专门补这块的。它要求你针对每个字段把最小值、最大值、超过最大值、靠近最大值的值统统拉出来单独测一遍。举个例子如果一个订单号字段允许 6 到 20 位数字边界值就至少是5 位数字、6 位数字、20 位数字、21 位数字。而如果这个字段允许 64 位整数范围内的数字那你得注意 19 位左右的数据会不会被解析成浮点、会不会在序列化过程中丢精度。回到标题这串99999999999999它在一个系统里的身份完全取决于这个字段的规则如果字段是验证码6 位它是越界如果是手机号11 位它超长且号段非法如果是 64 位整数范围内的编号它能被 Long 接受但可能被 int 变量接住时直接异常如果字段允许浮点它又可能因为精度问题在链路下游变成一个不完全相等的数。所以你才会看到同一个输入在 A 系统正常、在 B 系统报错、在 C 系统存进去但读出来已经变了。拆解这类问题的第一步就是先别急着下“用户乱填”的结论而是把这条输入放到每个节点的字段规则里去对一遍看它究竟踩了哪几道边界。1.2 为什么“直接拦截”根本不算处理完问题很多产品经理遇到这种一串 9 的输入第一反应都是那就在前端限制长度超过 11 位就不让输入。这个方案听着省事实际上是最偷懒的做法因为它把所有问题都藏在了用户提交前一旦碰到绕过前端的渠道整条链路照样裸奔。真实生产环境里数据从来不只从“用户网页填写”这一个入口进来。批量导入、定时任务回源、消息队列消费、内部系统接口调用、运营后台手工修正都可能把一个看上去“根本不该出现”的值带到核心链路里。我在实际项目中见过有人从前端拦截得很好结果换到 Excel 批量导入通道后一串 20 位的 9 被当作数值型载入导入工具内部把它转成了科学计数法再转回字符串时已经变成1E19这种脏数据最后竟然真的进了生产库让下游对账脚本跑了整整一夜。事后追溯时才想起来原来这个导入通道只做了“必填项有没有填”的校验根本没做长度和格式校验。这就是为什么“拦截掉不是终点”。比拦截更重要的是不管你打算接受这条输入还是拒绝这条输入每个入口、每一层都要有独立的、一致的校验能力。输入校验应该被当成一个横切关注点而不是某个表单页面的事。设计系统时你要先想清楚三个问题这条数据的每一层会用什么类型接如果某个字段只接受 32 位整数那超过 21 亿的数字进来是报错还是溢出如果某个字段是要传给下游展示用的编号它应该用字符串还是数值在接口里传输这三个问题想清楚了这串99999999999999的测试价值就出来了——它不只是测试用例它是整个输入体系的探针。2. 核心细节解析与实操要点2.1 数字在系统里最容易先“变味”类型和精度的隐患一串99999999999999进入系统第一个分岔路口就是“它被定义成什么类型”。如果后端接口文档里写的是 String那它顶多算一个超长字符串问题相对可控最多需要判断长度是否合法。如果接口文档里写的是 Long那 14 位数字确实还在 Java Long 的范围内能被正常解析但这只说明接收端没事不意味着链路里所有环节都有 64 位整数的容纳能力。我在实际排障中踩过最深的一个坑是接口定义用了 Long 接收一个“业务编号”但内部有一个很老的 C 语言写的模块编号字段还是 32 位 int。前端传一个正常编号还行某次因为测试环境数据异常上游直接推过来一串99999999999999Java 层解析成功后继续往下游调用下游模块拿到这个值以后发生了溢出变成一个小得诡异的负数系统也没报错后续逻辑拿着这个负数去查缓存、做幂等、发消息最后把一个订单状态改错了数据对不上账才发现问题。这种问题比异常难查得多因为接口没有抛异常它只是产生了错误的业务语义。还有一个更常见的坑叫“数字精度被浮点吃掉”。比如你用 JSON 格式调外部接口字段在文档里写的是 number前端拿到后用 JavaScript 解析JavaScript 的 Number 是双精度浮点它只能安全地精确表示-2^53到2^53之间的整数。超出这个范围的数字哪怕只是多了一位尾数就可能被悄悄改掉。Java 的 Long 最大能到 19 位很多接口就敢把 Long 类型的 ID 直接输出给前端结果前端拿到的往往已经不是原来的值了。处理方式业界早就有统一手势分布式系统里超过 JavaScript 安全整数的 ID、编号、流水号一律用字符串格式传输和存储不要为了省一点存储空间把它转成数值类型。作为参考我自己常用的判断标准是这个字段在业务里是“拿来算的”还是“拿来认的”。如果是算的比如金额、数量、折扣那它本质上是数值要仔细定义精度和舍入规则如果是认的比如订单号、用户 ID、交易流水号、证件号码、手机号那它就是标识符不是数学意义上的数必须按字符串处理。生产环境里绝大部分由99999999999999引发的事故问题本质都是把一个“用来认的”字段错误地定义成了“用来算的”类型。2.2 常见校验器为什么守不住这串 9现在大多数后端框架都有参数校验组件Java 有 Bean ValidationPython 有各种 Schema 库Go 也有 validator。但很多人用它们的方式都是“对着字段加注解”并没有理解校验器的真实作用边界。以 Java 为例你写了一个Pattern(regexp ^\\d{11}$)它能拦掉“99999999999999”这种 14 位输入但它拦不掉另一类更隐蔽的风险如果字段类型本身是 Long框架在进入校验注解之前就已经把字符串转成 Long 了。也就是说一个超过 Long 范围的 20 位数字会在类型转换阶段直接抛异常根本走不到正则校验那一步。这种异常返回给调用方的报错通常晦涩难懂用户看到的是“系统错误”研发看到的是NumberFormatException两边都很痛苦。再往前一层前端的maxlength和typenumber更是只能算提示。maxlength只对用户键盘输入生效用过自动化脚本、Postman、爬虫的人都能绕过typenumber在某些浏览器里甚至允许用户输入e因为浮点数允许科学计数法一个“数字输入框”的结果可能是9e20你拿它按字符串长度去校验也会出偏差。真实做法应该是前端限制长度只是为了用户体验和数据干净真正的校验必须发生在后端接口里而且必须把“先定类型、再定长度、后定格式”这个顺序做对。实际操作建议能接收多种输入形态的字段最好不要一上来就用数值类型接收。接口层多用String接收业务层再用明确逻辑去转换和判断。比如手机号、账号这类字段在 DTO 里定义为 String在持久层也用 VARCHAR 存中间不要碰Integer.parseInt()也不要让它被 ORM 自动转成数值列。这样做看起来多了一个类型转换步骤实际上换来的是全链路的可控。校验顺序也应该是先判空、再判长度上限、然后判字符集、最后做业务规则校验比如手机号号段。只要长度和字符集没过就没有必要继续执行后面的正则解析这样既安全又高效。2.3 别把边界输入一律当成恶意数据还有一类情况容易误伤某些业务字段本身就需要很长的数字串。身份证号是 18 位社会信用代码是 18 位很多支付机构内部生成的交易参考号是 20 位以上甚至有人把自己的银行卡号、护照号、学号原样填进来。如果只是简单粗暴地限制“最多 20 位数字”一看到超过 11 位就拦那正常用户就遭殃了。我在一个客服工单系统里就见过一个用户把一串 9 粘贴到手机号输入框后系统提示格式错误这本身没错但这段输入被存进了“备注”字段而备注字段的上游校验逻辑没有做长度限制导致这一条几 KB 的备注在后续全文检索服务里被反复分析最后把 ES 集群的 CPU 打满了。所以处理边界输入的核心原则不是“一刀切拒绝”而是“按字段真实的业务约束去判断”。如果一个字段允许 1 到 200 个任意字符那99999999999999完全合法系统要能正常存取和展示如果一个字段只允许 6 位短信验证码那 14 位 9 必须被友好拒绝并提示用户“验证码为 6 位数字”如果一个字段是业务编号但下游系统只能处理 32 位整数那这个问题就不是用户的错而是系统设计的错你应该在架构层面把编号升级为 String而不是反过来责怪用户输入了特殊值。系统性思维的核心是每一种输入都能落在明确、自洽的处理规则里而不是依赖“用户大概不会这么填”的侥幸。3. 实操过程与核心环节实现3.1 搭建一个可复现的“边界值探针”测试方案我平时遇到这类问题会单独建一个“输入探针”用例集而不是临时手工测几个值。这里直接给你一套可以抄作业的方案以“用户提交客服工单表单里有手机号、客户编号、备注三个字段”为例。先准备一组探针数据它们覆盖了大部分边界情况normal:13800138000正常 11 位手机号min_plus:138001380010 位差一位max_exact:9999999999911 位全 9长度合法但号段非法over_len:99999999999999也就是今天讨论的一串 14 个 9超过 11 位手机号长度上限long_19:999999999999999999919 位处于 Java Long 边界附近over_long:9999999999999999999920 位超过 Java Long 范围scientific:9e20可能是某些数值输入框允许的科学计数法mixed:9999abcd9999混合字符empty: 空字符串space_padding:13800138000首尾带空格在测试接口时我强烈建议不要只在前端页面上测因为前端会拦截掉一部分输入导致你得不到后端真实反应。正确姿势是直接调接口用命令行工具或者 Postman 请求后端。curl -X POST https://your-api.example.com/helpdesk/ticket \ -H Content-Type: application/json \ -d {phone:99999999999999,customerNo:13800138000,remark:test}如果后端phone字段用的是 String 接收那这一条大概率能被正则拦下。但接下来要看的是异常返回格式是否友好、响应时间是否正常、日志打印有没有把整串输入完整记录。我遇到过接口确实返回了 400但研发在代码里写了log.info(invalid phone: phone)于是一串 14 位的 9 被完整写进日志如果攻击者一直换不同位数的 9 来请求日志文件会快速膨胀。输入校验的现场处理有时候比校验本身更要命。3.2 后端校验落地示例从“正则万能”到分层校验后端真正的校验逻辑我建议按“层次”来做不是只写一个正则。第一层是接口 DTO 的基本注解。以 Java 为例用Length和Pattern组合不要只依赖Pattern因为Pattern默认只做完全匹配校验如果字段还能传 null最好也显式加上。一个比较稳的写法是这样的public class HelpdeskTicketCreateRequest { NotBlank(message 手机号不能为空) Length(min 11, max 11, message 手机号长度必须为11位) Pattern(regexp ^1[3-9]\\d{9}$, message 手机号格式不正确) private String phone; Size(max 32, message 客户编号长度不能超过32位) Pattern(regexp ^[0-9A-Za-z_-]$, message 客户编号只能包含数字、字母、下划线和短横线) private String customerNo; Size(max 200, message 备注不能超过200字) private String remark; }但注意上面这个只能防住浏览器端的正常提交。如果业务里还有 MQ 消息、Excel 导入、内部 OpenAPI 这些入口同一套校验必须可以复用。我的做法是把校验逻辑抽成独立的Validator类DTO 注解和导入逻辑都调用同一个方法避免出现“网页入口校验了导入入口没校验”这种信息差。public class PhoneValidator { private static final Pattern PHONE_PATTERN Pattern.compile(^1[3-9]\\d{9}$); public static boolean isValid(String phone) { if (phone null) { return false; } // 先判长度再判格式避免无意义的正则匹配 if (phone.length() ! 11) { return false; } return PHONE_PATTERN.matcher(phone).matches(); } }如果你要的是更通用的“超长数字串”拦截逻辑那就不能用Long.parseLong用BigDecimal也不是好方案最稳妥的是保持字符串判断public class BizNumberValidator { private static final Pattern DIGITS_ONLY Pattern.compile(^\\d$); public static boolean isValid(String bizNumber, int maxLength) { if (bizNumber null || bizNumber.length() maxLength) { return false; } // 数字字符串必须走全数字校验防空格、防正负号、防科学计数法 return DIGITS_ONLY.matcher(bizNumber).matches(); } }这里有一个很多人容易写错的地方判断“是不是纯数字”让你用NumberUtils.isCreatable()或Integer.parseInt()对短数字没问题但一遇到超过 Long 范围的输入就会出现异常。而在边界值测试场景下“超长数字”恰恰是最需要处理的那个值。所以校验超长数字串时一定要先按字符集白名单过滤而不是先尝试转成数值再判断。3.3 数据库字段设计字符串还是数值直接影响事故概率校验做完之后数据入库前的字段设计也是一个关键决策点。如果一条业务编号在业务上没有任何“加法、减法、比较大小”的需求那数据库这一列就不要用INT、BIGINT、DOUBLE这些数值类型。很多从 Excel 或外部系统同步来的长数字一旦进入数值列MySQL 的BIGINT最大还能撑 19 位DOUBLE会在小数点后出现假精度DECIMAL虽然能存精度数但在没有“算”的需求时依然没必要。直接使用VARCHAR(32)存储是最稳的——它既能容纳任意位数的 9也能保留前导零未来业务上如果要扩展长度只需要改字段定义不需要改存储引擎和代码逻辑。数据库里有一类数据是“看着像数字本质是字符串”手机号就是这样典型例子。11 位手机号如果用BIGINT存确实不会溢出但手机号第一位是 0 的场景在少数国家存在一旦未来业务要支持国际手机号数值字段就完全废了。更别说查询条件必须不断做隐式类型转换可能导致索引失效。我在设计新表时有个习惯凡是字段名里带no、id、code、phone、card的只要它不是用来参与聚合计算的一律用VARCHAR。除非这个字段是数据库主键且确实由数据库自增生成才用BIGINT。这个习惯帮我挡掉了非常多“一串 9 入库变1E19”的诡异问题。3.4 链路中要补的“数值序列化”处理除了数据库接口返回给前端时也要考虑长数字的序列化问题。如果你后端返回给前端的订单号就是 Java 的Long类型字段值正好超过 JS 安全整数范围那前端拿到的数据在不知不觉中就已经错误了。前端只要拿这个订单号去发起下一次查询就会查到一个不存在的单子。解决方案在 Spring Boot 生态里很成熟给字段加一个自定义序列化器改成输出字符串public class OrderVO { JsonSerialize(using ToStringSerializer.class) private Long orderNo; // getter / setter 省略 }如果是 Fastjson 或者 Jackson 全局配置也可以打开“Long 转 String”的全局策略但要注意这会影响所有 Long 字段包括那些前端确实需要相加统计的字段。所以我个人的建议是只在明确的 VO 字段上做不搞全局一刀切。具体哪个字段要转判断标准就是前面说的“这个字段是给前端看的编号还是给前端算的数”编号转字符串数值保持数值系统才不会在传输链路上默认丢精度。4. 常见问题与排查技巧实录4.1 高频问题速查表我在这些年处理过的“一串 9”工单里把典型问题汇总成了一个速查表希望你遇到时能少走几步弯路。现象典型原因处理建议接口直接返回 500日志出现NumberFormatExceptionController 直接用Long或Integer接收数字字段输入超出范围DTO 字段改为String在业务层按需转换并捕获异常前端显示的编号尾部变成 000 或出现e19JSON 反序列化时 Long 被转成 Number前端超出 JS 安全整数范围大编号在 VO 里统一序列化为字符串Excel 里看到1E19导入后数据不准导入模板单元格为数值格式Excel 用浮点保存 15 位以上数字导入模板列设为文本格式后端做严格字符串长度校验日志文件一夜涨了几个 G接口报错或校验失败时日志把超长输入完整打印出来打印参数前先截断单条日志只保留前 200 个字符用户填 11 个 9系统没拦只校验了“长度等于 11”没校验号段/正则手机号校验增加^1[3-9]\d{9}$这种号段白名单正则这个字段前端限制了长度接口还是收到超长值调用方绕过页面直接用接口调用以后端校验为准前端长度限制只作为体验优化一组导入数据全部成功但编号串号导入工具把数值列转成 double精度丢失后多个编号相同编号统一用文本导入数据库存 VARCHAR必要时对编号列加唯一索引API 网关偶尔返回 413 或超时某字段没做长度限制恶意/异常请求携带超大字符串网关层统一限制请求体大小及关键 Header 长度4.2 从真实事故里学到的三条排查经验第一遇到这种 9 开头的长数字先别急着看代码逻辑先用 curl 直接打到后端接口复现问题现场。你在页面上看到的现象往往是好几层过滤后的结果直接打接口能最快定位到是哪一层拦截失败或哪一层转换异常。如果打接口也复现不了再看是不是网关、负载均衡或者 WAF 提前处理了。第二排查“校验明明做了但没生效”这类问题时重点检查每个环节使用的字段类型是否一致。很多时候接口文档定义的是 StringController 接收的也是 String结果内部调用的 RPC 请求体里定义的是 Long序列化时自动把 String 转成了 Long这一转就把长度问题和精度问题全带到下游。在跨服务调用时看到“同一字段两套类型”要高度警惕这是一个比边界值本身更普遍的隐患。第三建议在系统里加一个“参数长度分布”指标。不需要上很复杂的监控只需要在网关或入口过滤器里统计每个接口请求参数的长度最大值、分位数。一旦某个字段的请求长度突然出现远高于业务正常值的极值你就能在事故形成前收到告警。我自己在后端入口处统一加了记录逻辑当参数长度超过 1000 时会单独打点并截断日志。这个配置既不影响正常业务又能快速发现大量由超长字符串引发的风险。系统的稳定往往就是靠这些不起眼的边界处理堆出来的。4.3 一个小技巧新功能上线前先拿一串 9 去按一下提交后来我们团队形成了一条很朴素的约定凡是新需求里涉及表单、接口、导入模板只要字段里会出现“长得像数字的编号”上线前都必须拿一串 9 去提交一次。不需要设计复杂用例就是最简单的99999999999999看看系统会返回什么。如果弹出一个让人看不懂的报错说明错误提示没有做好如果后端日志出现一大段堆栈说明类型转换没兜住如果数据直接进了库、显示的时候变成科学计数法说明存储字段设计有问题。这一下看起来只是“多按了一次提交”实际价值在于逼着整个研发流程把输入边界当成一等公民来对待。不要觉得“用户不会这么填”现实世界里的用户、爬虫、自动化脚本、内部同事测试远比我们想象的更有创造力。每一次用边界值探一探系统都可能帮你少熬夜排查一个线上事故。
返回列表