☰
Jmeter接口测试图片验证码处理:OCR识别与登录关联实战
2026/10/13 16:08:50 网站建设 项目流程

做接口测试的人,十有八九都栽在过验证码这道坎上。登录、注册、找回密码、下单、投票,几乎所有需要防机器人刷的流程都卡着一道图片验证码。Jmeter脚本跑得顺不顺,往往就取决于你能不能稳定地让后面的登录请求带上一个合法的captcha参数。这篇文章我就围绕“Jmeter接口测试中对图片验证码的处理”这个场景,把从获取图片、识别内容到关联登录请求的整套方法拆开讲一遍,适合正在用Jmeter做接口自动化、却被验证码卡住的朋友,也适合想了解验证码识别方案取舍的测试开发。

1. 为什么接口自动化会卡在图片验证码上

1.1 验证码的本质与自动化脚本的天然矛盾

图片验证码的完整逻辑其实不算复杂:服务端生成一串随机字符,把它画成一张带干扰线的图片返回给前端,前端把图展示给用户,用户肉眼看清楚后输入,服务端拿用户输入和生成时保存的答案做比对,一致就放行。这个过程对真人来说只有几秒钟,但对自动化脚本来说就尴尬了——脚本本质上也是“机器”,它要模拟人干的事,可验证码恰恰就是用来分辨“你是不是人”的。

这个矛盾导致了接口测试里非常典型的连锁反应:脚本拿不到验证码,就没法完成登录;登录不成功,后面所有依赖token或Cookie的接口全跑不起来。所以不管你是做单接口调试,还是整链路回归,甚至是压测前准备数据,只要系统里有验证码,第一件事就是先想清楚怎么处理它。

还有个容易被忽略的细节:验证码接口往往不是只有登录才有。有些系统注册、换绑手机号、提交表单、领取优惠券都各自接了一个验证码,每个接口的返回结构还不一样。有的返回PNG图片流,有的返回JSON里嵌base64字符串,有的还需要先拿一个captchaId再去换图片。处理前建议先把系统的验证码接口全部梳理一遍,分类处理,而不是一套方案走天下。

1.2 三类主流处理方案对比

处理图片验证码的思路,我见过的基本可以归纳成三类:绕过、识别、借助外部识别服务。

绕过是最粗暴也最稳定的。测试环境里让开发把验证码校验开关关掉,或者直接在配置里写死一个万能码,比如“9527”,不管生成的验证码是什么,输入这个固定值就能通过。优点是脚本稳定到几乎不会挂,缺点是只能用在测试环境,而且要开发配合改配置,属于“请外援”性质的方案。

识别就是本文的重点了。脚本自动下载验证码图片,用OCR(光学字符识别)技术把图片里的字符读出来,再填到登录请求里去。优点是通用,场景不受环境限制,缺点是识别率受干扰线、字体、颜色、扭曲程度影响,不可能100%准确,需要配合重试机制才能跑得稳。

借助外部识别服务常见的是打码平台,把图片传上去,平台用真人或高精度模型识别后返回字符。适合大规模压测,识别率高,但要花钱,而且要考虑外部服务延迟。小规模功能测试用这个有点浪费。

我的建议是分场景组合:日常功能回归优先让开发开万能码,稳定省心;需要验证完整链路时用OCR;性能压测时直接绕过,千万别在压测里跑OCR,CPU消耗能让你怀疑人生。

2. Jmeter环境准备与验证码图片的获取

2.1 基础环境配置要点

Jmeter做接口测试,建议直接用5.x以上版本,配JDK8及以上。因为后面要写JSR223脚本,Groovy对Java语法的兼容性更好,老版本Jmeter自带的BeanShell虽然也能跑,但对中文和正则的支持都差一截,能用Groovy就用Groovy。

解压后有几个地方建议先改:bin目录下的jmeter.properties,把samplersresult.encoding=UTF-8这行注释去掉,不然中文乱码会在后面断言时坑你一把。界面语言可以在jmeter.bat里加-Duser.language=zh参数,或者直接在选项菜单里切换,这个看个人习惯。

测试计划的基础结构建议这样搭:

  • 测试计划(勾选独立线程组)
  • 线程组
  • 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里目前最成熟的就是Tesseract,Google维护,支持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_whitelist=0123456789abcdefghijklmnopqrstuvwxyz

如果输出结果和图片上的字符接近,说明识别方案可行;如果完全乱来,先别急着写脚本,而是检查图片本身的质量问题。

改进识别率的几个常用参数:

  • --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 识别失败自动重试

验证码识别失败是常态,所以必须做重试机制。我的做法是:用一个循环控制器包住“获取验证码 -> 识别 -> 登录断言”三个步骤,循环条件设置为“登录尚未成功且未超过最大次数”。

先在测试计划里定义一个变量loginSuccess=false,登录成功时通过断言里的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链路,改用接口获取预置token

5.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)这种方式在循环处理多张验证码时就特别方便,批量跑场景时能少写不少代码。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询