
做接口测试的人十有八九都栽在过验证码这道坎上。登录、注册、找回密码、下单、投票几乎所有需要防机器人刷的流程都卡着一道图片验证码。Jmeter脚本跑得顺不顺往往就取决于你能不能稳定地让后面的登录请求带上一个合法的captcha参数。这篇文章我就围绕“Jmeter接口测试中对图片验证码的处理”这个场景把从获取图片、识别内容到关联登录请求的整套方法拆开讲一遍适合正在用Jmeter做接口自动化、却被验证码卡住的朋友也适合想了解验证码识别方案取舍的测试开发。1. 为什么接口自动化会卡在图片验证码上1.1 验证码的本质与自动化脚本的天然矛盾图片验证码的完整逻辑其实不算复杂服务端生成一串随机字符把它画成一张带干扰线的图片返回给前端前端把图展示给用户用户肉眼看清楚后输入服务端拿用户输入和生成时保存的答案做比对一致就放行。这个过程对真人来说只有几秒钟但对自动化脚本来说就尴尬了——脚本本质上也是“机器”它要模拟人干的事可验证码恰恰就是用来分辨“你是不是人”的。这个矛盾导致了接口测试里非常典型的连锁反应脚本拿不到验证码就没法完成登录登录不成功后面所有依赖token或Cookie的接口全跑不起来。所以不管你是做单接口调试还是整链路回归甚至是压测前准备数据只要系统里有验证码第一件事就是先想清楚怎么处理它。还有个容易被忽略的细节验证码接口往往不是只有登录才有。有些系统注册、换绑手机号、提交表单、领取优惠券都各自接了一个验证码每个接口的返回结构还不一样。有的返回PNG图片流有的返回JSON里嵌base64字符串有的还需要先拿一个captchaId再去换图片。处理前建议先把系统的验证码接口全部梳理一遍分类处理而不是一套方案走天下。1.2 三类主流处理方案对比处理图片验证码的思路我见过的基本可以归纳成三类绕过、识别、借助外部识别服务。绕过是最粗暴也最稳定的。测试环境里让开发把验证码校验开关关掉或者直接在配置里写死一个万能码比如“9527”不管生成的验证码是什么输入这个固定值就能通过。优点是脚本稳定到几乎不会挂缺点是只能用在测试环境而且要开发配合改配置属于“请外援”性质的方案。识别就是本文的重点了。脚本自动下载验证码图片用OCR光学字符识别技术把图片里的字符读出来再填到登录请求里去。优点是通用场景不受环境限制缺点是识别率受干扰线、字体、颜色、扭曲程度影响不可能100%准确需要配合重试机制才能跑得稳。借助外部识别服务常见的是打码平台把图片传上去平台用真人或高精度模型识别后返回字符。适合大规模压测识别率高但要花钱而且要考虑外部服务延迟。小规模功能测试用这个有点浪费。我的建议是分场景组合日常功能回归优先让开发开万能码稳定省心需要验证完整链路时用OCR性能压测时直接绕过千万别在压测里跑OCRCPU消耗能让你怀疑人生。2. Jmeter环境准备与验证码图片的获取2.1 基础环境配置要点Jmeter做接口测试建议直接用5.x以上版本配JDK8及以上。因为后面要写JSR223脚本Groovy对Java语法的兼容性更好老版本Jmeter自带的BeanShell虽然也能跑但对中文和正则的支持都差一截能用Groovy就用Groovy。解压后有几个地方建议先改bin目录下的jmeter.properties把samplersresult.encodingUTF-8这行注释去掉不然中文乱码会在后面断言时坑你一把。界面语言可以在jmeter.bat里加-Duser.languagezh参数或者直接在选项菜单里切换这个看个人习惯。测试计划的基础结构建议这样搭测试计划勾选独立线程组线程组HTTP请求默认值统一填协议、域名、端口、编码HTTP头管理器获取验证码请求下挂后置处理器登录请求下挂断言查看结果树、断言结果监听器基础结构先搭好后面往里面填具体逻辑就行了。HTTP请求默认值这个组件特别值得用所有请求的公网地信息集中在里面维护脚本里每个请求只写路径和参数就行后期维护成本低很多。2.2 验证码图片的两种返回形式拿到验证码图片首先得搞清楚它是以什么形式返回的。我归纳下来就两种直接返回图片流。响应头里是Content-Type: image/png或者image/jpeg响应体就是图片的二进制内容。这种情况下Jmeter里普通的“查看结果树”是没法直接预览图片的需要把二进制响应保存成文件。返回JSON里带base64字符串。比如{code:200,data:{captchaId:abc123,image:data:image/png;base64,iVBORw0KGgo...}}。这种情况需要先把JSON里image字段的base64内容取出来去掉data:image/png;base64,这个前缀然后做Base64解码才能得到图片文件。这两种形式处理逻辑完全不同我建议先动手把那一个验证码接口的响应在查看结果树里看一遍确认类型后再写对应的脚本。2.3 用JSR223后置处理器保存验证码图片这里给出最常用的两种Groovy脚本放在“获取验证码”HTTP请求下的“后置处理器 - JSR223 PostProcessor”里。第一种针对图片流import org.apache.commons.io.FileUtils; byte[] bytes prev.getResponseData(); if (bytes ! null bytes.length 0) { FileUtils.writeByteArrayToFile(new File(D:/jmeter_work/captcha.png), bytes); log.info(验证码图片已保存大小 bytes.length); } else { log.warn(响应体为空未保存图片); }第二种针对base64字符串import java.nio.file.Files; import java.nio.file.Paths; import java.util.Base64; String json prev.getResponseDataAsString(); def matcher (json ~ /base64,([^])/); if (matcher.find()) { byte[] imgBytes Base64.getDecoder().decode(matcher.group(1)); Files.write(Paths.get(D:/jmeter_work/captcha.png), imgBytes); log.info(base64图片已解码保存); } else { log.warn(未匹配到base64数据原始响应: json); }脚本里用的prev是JSR223取样器的固定变量代表上一个取样器的结果。如果对Groovy不太熟记住两个关键方法就行prev.getResponseData()拿二进制prev.getResponseDataAsString()拿字符串。脚本跑完后去本地目录看有没有生成captcha.png有就说明图片获取这一步通了。这里有个坑我必须提一下保存路径一定要用绝对路径而且确保目录已经存在。不要写相对路径Jmeter的工作目录在不同启动方式下不固定相对路径很容易把图片写到你不知道的地方去。3. OCR识别验证码的完整实操3.1 OCR工具选型Tesseract和Tess4J怎么选图片拿到手了下一步就是识别。开源OCR里目前最成熟的就是TesseractGoogle维护支持100多种语言识别印刷体字符的效果在开源方案里算第一梯队。Jmeter里调用Tesseract有两条路第一条是直接用Groovy调命令行tesseract.exe。优点是实现简单代码量少缺点是要拼命令行参数Windows上路径带空格会各种踩坑而且执行过程不好在Jmeter日志里跟踪。第二条是用Tess4J它是Tesseract的Java封装在JSR223脚本里创建Tesseract对象调用doOCR方法就能识别参数直接set进去逻辑清晰多了。缺点是需要提前把jar包放进Jmeter的lib目录还要准备语言包tessdata。我自己一直用Tess4J稳定性和可维护性都更好。命令行方式偶尔救场可以常规脚本还是别用了。3.2 Tesseract命令行快速验证正式写脚本前我强烈建议先用命令行验证一遍你自己的验证码图片能不能识别出来。这一步能帮你快速判断方向对不对节省大量调试时间。在命令行执行tesseract D:/jmeter_work/captcha.png stdout -l eng --psm 7 -c tessedit_char_whitelist0123456789abcdefghijklmnopqrstuvwxyz如果输出结果和图片上的字符接近说明识别方案可行如果完全乱来先别急着写脚本而是检查图片本身的质量问题。改进识别率的几个常用参数--psm 7: 把图片当作单行文本处理验证码基本都是单行字符适合这个模式-c tessedit_char_whitelist: 指定字符白名单比如纯数字验证码就只写数字能大幅减少误识别-l eng: 指定语言英文验证码用eng中文用chi_sim如果白名单设好之后识别率还是很差问题基本就出在图片预处理上需要先做图像处理再去识别。3.3 图片预处理灰度化、二值化、去噪、放大验证码生成方为了防OCR会加各种干扰背景噪点、干扰线、字符扭曲、颜色渐变。Tesseract对干净的黑底白字图片识别率很高对这些干扰图片就明显头晕。所以预处理是识别率提升的关键一环。标准的预处理流程四步走灰度化把彩色图片变成灰度图。因为颜色信息对识别字符没有帮助反而会引入干扰。灰度化之后每个像素就一个亮度值。二值化把灰度图变成黑白两色图。设定一个阈值高于阈值的变白低于阈值的变黑。这一步能去掉背景和干扰线的颜色差异。阈值的选择讲究我一般固定用127如果识别不理想再调。去噪去除图片中孤立的噪点。常见算法是扫描每个像素如果它和周围像素的颜色差异很大大概率是噪点直接把它改成背景色。放大把图片放大2到3倍。Tesseract对稍微大一点的字符识别更准放大后再识别经常能救回一两个被扭曲的字符。在Jmeter里做预处理可以直接用Java的javax.imageio.ImageIO读图再用BufferedImage遍历像素做灰度化和二值化。如果图片干扰特别复杂建议单独写一个Java工具类打成jar包放进Jmeter的lib避免在脚本里堆一大坨图像处理代码。3.4 JSR223中调用Tess4J识别验证码环境准备好后在“获取验证码”请求的后置处理器里紧接着保存图片的脚本后面再加一个JSR223 PostProcessor识别并存储结果到变量import net.sourceforge.tess4j.Tesseract; import net.sourceforge.tess4j.TesseractException; import java.io.File; Tesseract instance new Tesseract(); instance.setDatapath(D:/jmeter_work/tessdata); instance.setLanguage(eng); instance.setTessVariable(tessedit_char_whitelist, 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz); instance.setTessVariable(tessedit_psm, 7); try { String result instance.doOCR(new File(D:/jmeter_work/captcha.png)); result result.replaceAll([^a-zA-Z0-9], ); log.info(识别结果 result); vars.put(captcha, result); } catch (TesseractException e) { log.error(OCR识别失败: e.getMessage()); }这里解释几个关键点setDatapath必须指向存放tessdata的目录也就是你放eng.traineddata文件的那个文件夹写错路径会直接报错。setTessVariable(tessedit_char_whitelist, ...)是重中之重。如果你的验证码只含数字白名单设为0123456789就够了。如果含大小写字母和数字就用上面的完整白名单。很多新手识别率低就是因为没设置这个参数Tesseract把各种标点符号也当成候选字符误识别自然多。识别完成后用正则replaceAll([^a-zA-Z0-9], )把结果里的空格、换行、特殊符号清理干净。Tesseract输出经常带换行符不清理的话最后提交参数时会带着奇怪的字符。最后用vars.put(captcha, result)把识别结果存入变量供后面的登录请求引用。这是Jmeter前后请求传递数据的最常用方式。注意变量名尽量起得清晰一点别叫var1、var2这种。脚本多了以后看到captcha和token总比看到a和b好排查。3.5 登录请求如何关联识别结果识别结果存入${captcha}后登录请求里直接引用即可。如果你的系统还需要captchaId在获取验证码后的正则表达式提取器里把captchaId也提取出来保存成变量。假设登录接口的请求参数如下username: ${user} password: ${pwd} captcha: ${captcha} captchaId: ${captchaId}在HTTP请求的“参数”表格里填上这些就行。Jmeter会在发送请求前自动做变量替换。登录请求发送后如果识别对了服务端会返回登录成功标志或者token如果识别错了返回验证码错误。光有请求不够还必须有断言来识别是否成功这就是下一章要讲的内容。4. 用断言和重试机制提升脚本稳定性4.1 用JSR223断言精确判断登录成功与否OCR识别率不可能做到100%所以脚本里一定要有“这次到底成没成”的判断逻辑。Jmeter里最简单的做法是在登录请求下挂一个“断言 - JSR223 Assertion”。String response prev.getResponseDataAsString(); if (response.contains(登录成功) || response.contains(token)) { prev.setSuccessful(true); log.info(登录成功); } else { prev.setSuccessful(false); log.info(登录失败响应内容: response); }这段脚本的逻辑拿到登录接口的响应包含“登录成功”或者“token”字样就置为成功否则置为失败并在日志里打印响应内容方便排查。有个特别容易踩的坑prev.setSuccessful(false)之后一定要确保后面的代码不再把它改回去。如果你在同一个断言里写了多个判断分支或者用了finally块注意别把状态覆盖了。我见过有同事在finally里写prev.setSuccessful(true)结果所有失败请求全被判成成功。另外用log.info打印响应内容时注意不要打日志打得太频繁。调试时打没问题压测时每条失败都打会撑爆日志文件。4.2 响应里的错误码怎么定位很多系统的登录接口返回统一格式的JSON比如{code:401,message:未登录,请登录!} {code:200,message:登录成功,data:{token:xxx}}这种情况下用正则或者JSON断言校验code字段更规范。Jmeter自带JSON断言插件也可以在JSR223里用JsonSlurper解析import groovy.json.JsonSlurper; String resp prev.getResponseDataAsString(); def json new JsonSlurper().parseText(resp); if (json.code 200 json.data.token ! null) { vars.put(token, json.data.token); prev.setSuccessful(true); } else { prev.setSuccessful(false); log.info(登录失败: code json.code , msg json.message); }如果看到{code:401,message:未登录,请登录!}这种报错先别急着怀疑验证码识别大概率是前置请求没成功或者Cookie/token没携带。Jmeter里就是检查HTTP头管理器里有没有加对应的Header以及提取token的正则是不是写对了。4.3 识别失败自动重试验证码识别失败是常态所以必须做重试机制。我的做法是用一个循环控制器包住“获取验证码 - 识别 - 登录断言”三个步骤循环条件设置为“登录尚未成功且未超过最大次数”。先在测试计划里定义一个变量loginSuccessfalse登录成功时通过断言里的vars.put(loginSuccess,true)更新它。循环控制器的循环条件写${__jexl3(!(${loginSuccess} true) ${loopCount} 5,)}同时在线程组里设置循环次数上限避免死循环。这样做的好处是一次失败后脚本自动重新获取新验证码重新识别重新登录直到成功或达到上限。实测下来如果单次识别率在70%左右重试三次后整体成功率能到97%以上基本可以满足功能回归的需求。注意重试时要确认验证码是否过期。有些系统验证码有效期只有60秒如果脚本在获取验证码和登录之间做了太多事情比如打印日志、调试参数可能登录时验证码已经过期了这时候不是识别的问题是时效性的问题。有这种情况的话把中间步骤精简一下。5. 常见问题与排查技巧实录5.1 问题速查表我整理了这段时间被问得最多的问题直接列成表格方便对照现象可能原因解决方案保存图片时日志报空指针响应体为空或获取方式不对确认请求响应是图片流还是JSON检查响应码是否200识别结果总是空字符串tessdata路径配置错误setDatapath必须指向含eng.traineddata的目录识别结果乱码或带标点没有设置白名单补上tessedit_char_whitelist参数识别结果缺字符图片干扰严重做图像预处理放大图片调整二值化阈值登录一直提示验证码错误验证码过期或captchaId未提交检查验证码有效期确认captchaId字段是否传递登录返回401未登录Cookie/token未携带检查前置登录是否成功正则提取器是否写对并发跑的时候大量失败OCR太慢且验证码被重复使用压测场景不走OCR链路改用接口获取预置token5.2 并发场景下千万别跑OCR链路这一条单独拎出来说因为太重要了。如果你用Jmeter做登录接口的性能测试脚本里还带着“下载验证码 - OCR识别 - 提交登录”这套完整链路压测一开先不说Jmeter所在机器的CPU会被OCR进程吃到100%就是服务端那边大量并发的验证码生成和校验本身也会成为瓶颈压出来的数据完全不代表真实业务水平。正确的做法是压测前先准备好压测账号或者通过一个专门的“测试环境跳过验证码”的开关来压。验证码校验不应该出现在性能测试的路径里这是我在实际项目里反复踩坑得出来的结论。5.3 中文验证码的应对策略如果验证码是中文Tesseract默认的eng语言包就完全不够用了需要下载chi_sim.traineddata语言包放到tessdata目录然后在代码里把语言set成chi_sim。但说实话中文验证码的识别率即使配了语言包也不会太高因为中文字符笔画多、结构复杂加上扭曲和干扰线后OCR非常吃力。我的建议是能跟开发协商就尽量协商把验证码改成纯数字或字母数字组合这对开发来说改动不大但对自动化测试来说是质变。如果协商不了至少争取测试环境开万能码。实在不行就只能靠预处理和多轮重试来提高整体成功率单次识别率别指望太高。提示不管用什么方案验证码这个环节都要在脚本里设计兜底逻辑。识别失败不要紧重试几次能过就行千万不要让脚本因为一次识别失败就中断整个测试流程。6. 几点个人经验与心得做接口测试这几年我最大的体会是验证码这个坑技术永远排第二位业务沟通才排第一位。拿到一个带验证码的需求第一件事不是打开Jmeter而是先弄清楚验证码是哪几种、哪几个接口有、验证码有效期多长、有没有测试开关、验证码过期后后端返回什么结构。这些信息比任何OCR参数都值钱。如果开发能配合在测试环境加一个“验证码旁路开关”整个测试脚本的稳定性会立刻拉到99%以上OCR那些事就只剩“了解原理”的程度。如果必须走OCR我的经验是先拿十张验证码图片离线测试识别率低于60%就不要硬上了先把预处理做好再说。最后分享一个小技巧Jmeter里用JSR223脚本时能用vars.put就尽量别硬编码变量名。Groovy脚本里动态拼接变量名很麻烦但vars.get(captcha_ i)这种方式在循环处理多张验证码时就特别方便批量跑场景时能少写不少代码。