Jmeter接口测试验证码登录全攻略:短信、图形验证码与Token关联
做接口测试的人迟早会撞上验证码这道墙。Jmeter脚本写得好好的一到登录接口就卡住验证码拿不到登录进不去后面所有业务流程全部瘫痪。这不是脚本问题而是验证码本身就是一个反自动化设计。我当年第一次做登录压测在这个环节整整卡了一天后来把整套处理思路理顺之后验证码反而成了最不让人担心的部分。这篇文章就把验证码登录接口从功能验证、链路联调到并发压测的完整Jmeter处理方法写清楚覆盖短信验证码和图形验证码两种最常见形态顺便带上登录成功之后Token和Cookie的关联用法给还没趟过这摊水的朋友一份可以直接照抄的作业。1. 验证码登录接口先想清楚三件事测的到底是什么1.1 验证码天生反自动化但你要测的偏偏是自动化验证码的设计初衷就是区分人类和机器图形验证码靠扭曲字体、干扰线和人脑识别短信验证码靠真实手机号和时效性来确权。这些机制在防御方面很有效但对自动化测试来说全是障碍。Jmeter本质上就是机器跑自动化脚本就是高频率模拟请求恰恰是验证码最想拦下来的那类流量。理解了这层矛盾你就能明白为什么教程里第一步不是写脚本而是先商量怎么绕过。跟开发团队确认验证码的接入方式、校验逻辑、存储位置和使用限制比拿到接口文档更重要。我遇到过不止一次接口文档写得明明白白但验证码接口在网关层还有额外限流导致测试过程中被误判成攻击流量整个压测数据全部作废。验证码关联的常见限制包括同一个手机号一分钟内只能发一次短信图形验证码加载一次就失效验证码五分钟过期连续错误五次锁定账号。这些限制对真实用户来说影响不大但对Jmeter这种高频工具就是致命的。做方案前先把这些限制摸清楚你才知道是要写参数化手机号还是要找开发开白名单。1.2 登录接口的产物是认证状态不是那个四位数很多人测验证码登录接口时容易陷进去一个误区总觉得测的重点是验证码本身。实际上验证码只是登录流程的一道闸门登录接口真正要测试的对象是认证结果也就是登录成功之后返回的那一串Token、Session、用户信息、权限标识。所以功能验证阶段你需要确认的核心用例是这几类正确验证码能不能登录成功错误验证码能不能被拦截过期验证码会不会放行验证码不传会不会有参数校验验证码重用会不会被识别。这些用例跑完之后真正的链路测试才开始带着登录拿到的令牌去访问需要身份才能看的接口验证权限边界和会话有效性。我建议你在Jmeter里把登录接口的返回结果和后续业务接口的执行结果关联起来看而不是只看登录本身。很多项目登录接口单测是通的但是Token签发后时效性设置有问题或者返回头里的Cookie与响应体里的Token不是同一条会话链这些坑只有做完整链路才会暴露。1.3 功能验证、链路联调、并发压测三个场景三种打法验证码登录接口的处理方式取决于你当前在做哪一类测试。三种场景的目标和方法是完全不一样的。测试场景核心目标验证码处理建议主要关注点单接口功能验证验证登录接口的各类用例关闭验证码或使用OCR识别参数校验、错误码、Token生成链路冒烟联调登录贯穿后续业务流万能验证码或固定测试码登录态传递、接口间依赖并发性能压测验证系统在高并发下的表现关闭验证码或万能验证码响应时间、错误率、资源占用性能压测最怕的就是大并发去触发验证码的防刷限流导致错误率虚高。你分不清报错到底是被限流拦截了还是应用服务器真的扛不住压力。所以压测登录接口时我强烈建议默认走“拆码”通道让验证码这个环节不参与性能计算你测出来的数据才反映真正的后端处理能力。至于OCR识别验证码的方式仅建议在功能联调阶段尝试验证用别让它拖垮压测进度。2. 环境允许时优先“拆码”测试环境的三种验证码退路2.1 最省事的方式直接在服务端关闭验证码开关很多大型系统的验证码功能是由独立的验证服务提供的开发框架里通常都留了配置开关。你可以在测试环境的配置中心把验证码功能关闭或者把验证码校验逻辑做成一个可插拔组件测试环境里直接拔掉。操作上你要注意一点关闭验证码开关不能影响登录接口本身的其他校验逻辑。我见过有人图省事把整个认证服务降级了结果登录接口的密码校验也跟着失效测试环境直接裸奔。正确做法是只关掉验证码那一层保留账号密码、设备指纹、参数校验等所有安全逻辑这样测出来的登录接口行为才接近真实生产表现。另外关闭验证码开关后一定要在测试环境里留一条能开回来的通道。因为偶尔需要现场验证验证码相关缺陷这时候你再去改配置重启服务就慢了。比较好的做法是做成开关型配置改完配置自动热加载不需要重启服务。2.2 万能验证码压测时的最佳搭档万能验证码是目前压测场景下用得最多的方案思路很简单服务端在验证码校验逻辑里预留一个固定值当输入的验证码等于这个值的时候直接放行。比如代码里写死如果验证码等于“888888”跳过验证直接通过。这个方案的优点在于验证码环节仍然在逻辑上被调用了只是被放行了你可以完整走通“传参—校验—放行—签发Token”整个链路不会因为没有验证码导致流程和真实场景不一致。我在性能压测里基本都推荐这种方案因为压测数据比较干净不会因为验证码识别失败引入额外的错误率。但万能验证码必须只允许在测试环境生效。有些开发把万能验证码挂在生产环境的开关上没有下线这太危险了等于给所有攻击者留了一扇后门。作为测试人员你应当在用例中设计一条验证生产环境不存在万能码的检查项。2.3 白名单参数把验证码从认证链条里摘出去还有一类系统设计得更灵活会预留一个调试参数比如请求头里带X-Debug-Validation: bypass后端在解析到该参数时直接跳过验证码校验。这种方案在联调和性能压测阶段都很好用因为它不需要业务侧改配置只需要在HTTP请求中增加一个管理器不影响其他接口。用白名单参数要留意的是路由层防护这个debug通道必须被网关拦截不能穿透到生产。你可以在测试计划里区分环境变量生产环境压根就不允许传入这个参数甚至可以专门写一个脚本去探测生产环境的LTS入口确保白名单通道失效。这几种“拆码”方式的本质都是调整校验链路没有优劣之分只有适不适合当前项目状态。如果项目里已经有万能验证码就用万能验证码如果没有跟开发沟通不改代码就用白名单参数都不行就只能走数据库取码或者OCR识别的路线也就是下面两章要展开的内容。3. 短信验证码链路发送接口、数据库取码、登录参数引用3.1 第一步先把发送验证码的HTTP请求搭起来短信验证码的完整链路是先调发送验证码接口系统往目标手机号发一条短消息然后你拿到短信里的验证码再调登录接口提交。Jmeter跑这个链路最怕的就是你手动等短信、手动敲验证码一旦上了循环和并发就完全没法玩。所以我们要把整个链路脚本化。先搭最基础的线程组和HTTP请求。线程组里加一个HTTP请求指向发送验证码接口按常见项目推测大概长这样POST /api/sms/send Content-Type: application/json { mobile: 13800138000, scene: login, type: 1 }实际项目中发送验证码往往需要携带图形验证码的校验结果或者有行为校验参数比如设备标识、时间戳、随机串。你需要先跟开发确认这一步最容易遗漏的是请求头里的Token、签名参数。我见过有人调发送接口一直报“风控拦截”最后发现是少传了一个设备的指纹头。发送接口的响应通常只返回成功标记和流水号验证码本身不在响应体里而是落到短信通道和数据库里。如果你们的验证码系统在测试环境有专门的短信记录服务直接从数据库里捞是最稳的。3.2 JDBC直连数据库验证码的真正来源既然验证码不返回在接口响应里我们就去数据库里找。Jmeter提供了JDBC组件包括JDBC Connection Configuration和JDBC Request用起来和数据库客户端一样。在测试计划里添加JDBC Connection Configuration关键配置如下JDBC Driver classcom.mysql.cj.jdbc.DriverMySQL环境Database URLjdbc:mysql://测试库地址:3306/业务库名?useSSLfalseserverTimezoneUTCUsername测试库账号Password测试库密码Variable NamedbConfig这个Variable Name非常重要后面的JDBC Request要靠这个名字来引用连接池。如果Jmeter执行时报“无法加载JDBC驱动”说明lib目录下没有对应的数据库驱动包去下载对应版本的驱动jar放到jmeter的lib目录重启Jmeter即可。接着添加JDBC Request组件Variable NamedbConfigSQL QuerySELECT code FROM t_sms_record WHERE mobile${mobile} ORDER BY send_time DESC LIMIT 1Variable Namessms_codeResult variable namesms_resultJDBC Request执行完毕之后查询到的验证码就存到了变量sms_code里后面登录接口直接引用它。查询时要注意两点一是表名和字段名以实际项目为准我写的是最常见命名你要去项目的数据库里确认二是排序一定要按时间倒序LIMIT 1取最新的一条因为短信验证码通常有多个历史记录取错一条就会登录失败。3.3 登录请求引用验证码用调试断言快速定位拿到sms_code之后登录接口长这样POST /api/login Content-Type: application/json { mobile: ${mobile}, code: ${sms_code}, clientType: WEB }跑通之后建议在JDBC Request后面加一个JSR223断言或者查看结果树先确认sms_code到底有没有取到值。我习惯在JSR223断言里写一行log.info(当前验证码 vars.get(sms_code))然后在日志里直接看结果。用BeanShell断言也能做这件事但现在更推荐JSR223Groovy因为BeanShell跑得慢压测时容易拖慢整体性能。短信链路里有一个隐蔽的坑验证码可能有有效期默认一般是5分钟或10分钟但并发压测时如果线程排队等待部分线程拿到的验证码已经过期了。所以如果你在做压测短信验证码链路其实不太适合作为压测入口这也是我前面强调压测用万能验证码的原因。链路全部跑通之后别忘了验证码的重置逻辑登录成功后验证码是否立即失效登录失败后验证码还能不能用。这些用例看起来小但是和用户实际体验直接相关。4. 图形验证码链路取图、OCR识别、一次登录测试跑通4.1 获取图形验证码图片和captchaId图形验证码的接口交互和短信验证码不太一样。它通常有两个关键信息一个是captchaId用来标识本次验证码的会话一个是图片内容可能是Base64字符串也可能是一个图片URL。Jmeter里访问验证码接口的方式很简单用HTTP请求即可GET /api/captcha?timestamp1693250000000加timestamp参数主要是为了绕过缓存否则可能每次请求都返回同一个验证码。有些项目在网关层做了缓存如果你不附加随机参数图片内容会一直是同一张那后面对比识别结果和登录结果就没意义了。接口返回的JSON结构常见形态是这样{ code: 0, data: { captchaId: a8c1f9d3-..., img: data:image/png;base64,iVBORw0KGgo... } }在Jmeter里处理Base64图片不能直接把字符串塞给OCR服务需要先解码成图片文件。可以在HTTP请求之后加一个JSR223后置处理器用Groovy脚本把图片和captchaId提取出来。4.2 OCR识别用JSR223采样器调用本地识别服务拿到图片数据之后下一步是识别出验证码明文。我常用的方案是把图片保存到本地然后调用一个本地的OCR识别服务把识别结果返回给Jmeter。在“获取验证码”的HTTP请求下添加一个JSR223后置处理器写解码逻辑import java.util.Base64 import groovy.json.JsonSlurper def resp prev.getResponseDataAsString() def data new JsonSlurper().parseText(resp) vars.put(captchaId, data.data.captchaId) def imgStr data.data.img if (imgStr.startsWith(data:image)) { imgStr imgStr.substring(imgStr.indexOf(,) 1) } byte[] imgBytes Base64.getDecoder().decode(imgStr) def imgFile new File(captcha_ vars.get(captchaId) .png) imgFile.bytes imgBytes props.put(captchaImgPath, imgFile.getAbsolutePath())然后再添加一个JSR223采样器读取本地图片并调用OCR HTTP服务import java.io.File def f new File(props.get(captchaImgPath)) def url new URL(http://127.0.0.1:8000/ocr) def conn url.openConnection() conn.requestMethod POST conn.doOutput true conn.setRequestProperty(Content-Type, application/octet-stream) conn.outputStream.write(f.bytes) conn.outputStream.flush() conn.outputStream.close() def result conn.inputStream.text def json new groovy.json.JsonSlurper().parseText(result) vars.put(captchaCode, json.code)执行完这个采样器识别出的验证码就存在变量captchaCode里。如果你的OCR服务返回的是纯字符串那就更简单直接vars.put(captchaCode, result.trim())就行。4.3 图形验证码链路的几个真实踩坑点第一验证码通常是一次一用。线程A获取了验证码但登录失败这个captchaId和code就废了线程B再用必然会失败。排查问题时别把这种失败归因到并发能力上先把验证码关联关系理清。第二OCR识别率不可能100%。最简单的纯数字四位验证码识别率90%以上是可以的一旦加了扭曲、噪点、干扰线识别率可能掉到50%以下。识别失败就登录失败进而拖高错误率。所以OCR方案只适合做单接口功能联调不适合做性能压测数据入口。第三本地保存图片的路径要和脚本运行环境匹配。如果Jmeter脚本要提交到持续集成平台上运行图片要保存到相对路径或者临时目录不要用硬编码的绝对路径否则换台机器就崩。第四图形验证码接口有时会走HTTPSJmeter访问HTTPS接口需要先导入安全证书。录制脚本时如果遇到HTTPS请求报证书错误把证书导入到Jmeter的lib/ext目录里的cacert或者配置成忽略校验。这个问题网上资料很多我就不展开了但大家遇到类似报错时先往证书方向排查。5. 登录成功后别停手Token提取、Cookie会话和结果落盘5.1 从登录响应里提取TokenJSON提取器和正则二选一登录接口跑通只是第一步后续带着登录态去访问业务接口才是重点。绝大多数现代项目登录成功后都会返回TokenJmeter里提取Token最常用的工具是JSON提取器。在登录HTTP请求下添加后置处理器里的JSON提取器配置如下Apply toMain sample and sub-samplesJSON Path expression$.data.tokenVariable nameslogin_tokenDefault valueNOT_FOUND如果你的登录响应结构不是标准的data字段下挂token比如返回层级比较深或者Token藏在响应头的Authorization字段里就需要换成正则表达式提取器。正则表达式长这样token\s*:\s*([^])模板是$1$匹配数字填1变量名同样叫login_token。两种提取器各有利弊。JSON提取器依赖响应是合法JSON一旦响应被包装过比如返回了text/html的异常页提取器会失手正则表达式则对字符串匹配更直接适合从混合内容里抓数据。我的习惯是能解析JSON就用JSON提取器读起来清楚维护起来不容易出错。5.2 Cookie会话保持别让登录态在请求之间断裂很多老系统和服务端渲染的项目登录态的载体不是Token而是Cookie。Jmeter处理Cookie可以用HTTP Cookie管理器把它加在线程组下面勾选“自动保存Cookie”就行。加了Cookie管理器之后登录接口返回的Set-Cookie会被自动保存后续的HTTP请求会自动携带对应的Cookie值。这个组件解决的最大问题是会话漂移也就是每次请求创建新会话导致登录态丢失。有些系统比较特殊登录响应里同时返回Cookie和Token而且两个都校验。这时候你就需要把Token放到后续请求的请求头里一般在线程组下添加一个HTTP Header管理器加一行Authorization: Bearer ${login_token}。注意Header管理器添加在哪个作用域决定了带这个请求头的请求范围。我之前做过一个单点登录项目登录返回的Token要放到URL参数里而不是请求头里否则网关不认账。所以拿到登录响应后先别急着写死方案去看一下后续接口实际需要哪种鉴权方式多看几个接口再定。5.3 把提取结果写成文件为数据驱动和后续准备Jmeter提取出来的变量默认只在当前线程内有效一旦测试结束就没了。如果你想把这些Token保存下来后续做数据准备或交给其他工具使用需要主动把变量写进文件。在登录请求下加一个JSR223后置处理器用Groovy把Token追加到本地文件def f new File(tokens.txt) def w new FileWriter(f, true) w.write(vars.get(login_token) System.lineSeparator()) w.flush() w.close()这段脚本是追加写入不会覆盖之前的记录。对于并发压测场景每个线程都会执行一次写入最终文件里的Token数量应该和线程数一致。如果数量对不上就说明部分线程的登录请求没有成功取到Token需要回头排查登录失败原因。这个方法也叫提取JSON结果生成文件在数据准备环节非常常用比如你要生成一批真实的登录态给线上巡检用。6. 压测验证码登录接口线程数设计、错误率排查与报告解读6.1 压测登录接口的线程组怎么设计压测登录接口时线程组的设置要结合登录接口的特征来定。登录接口通常是无状态短链接、请求量大的类型主要消耗的是应用服务器的认证处理能力。我的建议是压测登录接口使用阶梯式线程模型线程组里先设50个线程、Ramp-up 10秒循环次数根据测试时长动态调整。不要一上来就拉500并发登录接口如果因为验证码限流或者密码错误锁定等机制产生错误你无法判断数据里的错误率到底是压出来的还是业务规则拦的。线程组的Loop Count如果设置成循环多次每个线程会重复执行登录这意味着每次循环都会重新触发验证码逻辑。如果验证码没有得到妥善处理比如走OCR识别一次要几百毫秒大量线程同时OCR很容易把识别服务打爆。所以压测时仍然建议打开万能验证码通道或者干脆关闭验证码校验把登录接口还原成一个纯粹的认证接口去压。6.2 用断言和日志挖出真实的失败原因登录压测里最让人头疼的错误信息是“验证码错误”。你以为全部问题都出在验证码上实际上这只是表象背后的真实原因可能五花八门。最常见的假性失败来自参数变量没取到值。比如手机号参数没有正确参数化所有线程都用了同一个号码服务端会先拦截重复验证码请求然后登录全部失败。你在查看结果树里会看到大量“验证码已失效”之类的提示其实根源是手机号没做参数化而不是验证码本身的问题。排查技巧是给登录请求加JSR223断言在断言里把关键变量打印出来log.info(手机号 vars.get(mobile)) log.info(验证码 vars.get(sms_code)) log.info(Token vars.get(login_token))加上之后跑一轮小并发去Jmeter日志里翻每一个线程的实际取值基本能定位是变量问题还是接口问题。断言写得好能把错误码、响应体、请求参数全部留痕压测结束后整理缺陷报告也有据可依。6.3 结果报告看什么响应时间分布比平均响应时间更有用Jmeter自带的聚合报告能看平均响应时间、错误率、吞吐量这几个核心指标。但在登录接口场景我建议你更关注响应时间的分布也就是P90、P95和P99。平均响应时间很容易被个别长尾请求拉高而P95能更真实地反映大多数用户的体验。登录接口还有一个需要关注的指标是错误率的构成。你可以搭配JSR223断言和结果树把失败请求单独筛选出来按错误码统计归类。比如业务错误码为10001的是验证码错误10003的是密码错误10007的是限流拦截。这种归类能直接告诉你系统瓶颈到底在哪一层。我个人的压测经验是跑登录压测的时候先小规模验证脚本正确性再上量级。宁可多跑三轮小并发也不要一次跑个大的然后数据全是废的。每次跑完先去查看结果树里成功和失败的请求长什么样确认判断逻辑无误再进下一轮。最后再分享一个我项目里用过的做法压测登录接口时顺手把应用服务器的Tomcat、数据库连接池、Redis连接数都抓一份曲线。登录接口是后端认证链路的总入口它一旦出问题往往不是接口本身的锅而是底下依赖的数据库连接池或缓存节点先扛不住了。多看一眼这些资源指标排查问题时候能少走不少弯路。