尧图精选

JMeter加密接口测试实战:AES与RSA签名解密全攻略

🕒 发布时间:2026/10/1 22:47:16 📁 来源:尧图网络
有没有碰上过这种场景开发扔来一份接口文档请求参数里写的是“data加密后的业务参数”“sign签名串”你问开发怎么加密他回你一句“看一下文档附录”。结果你打开JMeter填好URL参数用明文怼上去一执行服务端返回“签名错误”。我做接口测试这些年加密接口就像一堵墙拦住了不少刚入门的人。其实墙后面没什么黑魔法只要你理解了加密规则然后用JMeter的JSR223脚本把加密过程复现出来后面的请求、断言、压测都是一马平川。这篇文章会从最基础的JMeter环境讲起用两个实际案例——AES登录接口和RSA签名接口——把完整步骤拆开揉碎。不管你是第一次处理加密接口的测试新人还是在Postman和JMeter之间纠结怎么选的老手这篇都值得参考。1. 被加密挡住的接口测试问题出在哪1.1 真实场景拿到一个全是密文的接口我之前接过一个内部系统的登录接口文档上写得很简单POST /api/user/login请求参数是timestamp、data、sign。但data字段的值不是用户名密码而是“AES加密后的JSON字符串”sign是“MD5签名”。我用JMeter直接填明文请求服务端毫不客气地回了个sign check failed。那一刻我才意识到接口测试早就不是“填个URL、点个发送”那么简单。为了让数据在传输过程中不泄露现在很多业务系统都要求前端把请求体加密后端收到后先验签再解密。测试同学如果不在JMeter里复现同一套加密逻辑连请求都发不出去更别提做断言和压测了。这个问题的本质是JMeter默认只负责发HTTP请求它不关心你的业务数据是怎么变的。但JMeter提供了JSR223、BeanShell、前置处理器、断言组件正好给了我们“拦截请求前计算密文、拿到响应后解密校验”的能力。这就是我用JMeter做加密接口测试的切入点。1.2 JMeter凭什么能处理加密请求市面上能做接口测试的工具很多Postman、Apifox都能写脚本。但我是JMeter的忠实用户原因很简单JMeter是Java生态里的东西而Java本身就有完善的加密库支持。AES、RSA、MD5、SHA256这些算法标准JDK里全都有不需要额外装环境。再加上JMeter能跑Groovy脚本Groovy可以无缝调用Java类等于给我开了一个“加密实验室”。更实际的好处是JMeter不仅仅能做功能测试。同一个加密脚本功能测试时跑一遍压测时还能跑一千遍。如果你用Postman测通了加密接口到了压测阶段还是得回到JMeter或者改用其他压测工具那就等于学了两套东西。直接在JMeter里把加密逻辑沉淀下来后面所有测试活动都能复用这一套脚本。另外JMeter有非常灵活的变量体系vars是线程内变量props是全局属性配合前置处理器可以在请求发送前动态计算密文断言组件则可以在请求返回后对解密结果做校验。这套机制用来处理“先加密后请求、先解密再断言”的场景简直像量身定做的。1.3 加密接口测试的技术栈选型下面是我在实际项目中遇到的几类加密算法也对应到JMeter里的实现方式算法类型常见用途JMeter中使用方式AESCBC/ECB/GCM对称加密请求体加密、敏感字段加密JSR223 Groovy调用javax.crypto.CipherDES/3DES对称加密老系统接口同上SM4国密对称加密政务、金融类接口需要引入BouncyCastle或者国密SDKRSA非对称加密登录加密、加密传输密钥JSR223 Groovy调用KeyFactory/Cipher需要处理分段SM2国密非对称加密金融、CA认证需要引入国密依赖库MD5/SHA-1/SHA-256摘要算法签名、防篡改Java标准库或Apache Commons CodecHmacSHA256带密钥的摘要算法签名标准库Mac类我用得最多的组合是AES加密请求体 MD5或HmacSHA256签名。这套组合在移动端App接口里遍地都是。RSA则通常用来加密登录密码、支付密码这类“不放心交给对称加密”的字段或者用来加密对称密钥本身。确定好算法之后接下来的问题就是怎么让JMeter认识Java的加密类这就是环境准备的事了。2. 环境准备JDK、JMeter和加密库一个都不能少2.1 JMeter版本与JDK版本匹配如果你用的是JMeter 5.5以上的版本JDK至少需要8推荐用11。为什么要强调这个版本匹配因为我见过有人装了最新的JMeter 5.6但机器上还是JDK 1.7结果JMeter启动都起不来。JDK版本还直接影响代码写法JDK 8引入了java.util.Base64但JDK 7里没有这个类只能退而用javax.xml.bind.DatatypeConverter或者第三方库。我的建议是统一使用JDK 8或JDK 11这个组合在JMeter 5.x系列里最稳。JMeter本身的安装不复杂官网下载二进制包解压后设置好JAVA_HOME环境变量启动bin/jmeter即可。如果你第一次接触JMeter搜索“jmeter安装教程”会有很多步骤这里不展开但记住一点JMeter安装包一定是Apache官网下载别去第三方站点下乱七八糟的版本。2.2 把外部加密库放进JMeter的classpath虽然JDK自带AES、RSA、MD5的实现但很多项目签名用的是HmacSHA256或者国密SM2/SM4。这种时候就需要引入外部依赖。JMeter支持把jar包丢到两个目录lib/JMeter启动时会自动加载的公共库。lib/ext/JMeter扩展插件的目录也可以放自己的工具库。我习惯把Apache Commons Codec放到lib/ext下因为它提供了DigestUtils.md5Hex()这样的便捷方法写签名时能少写一大堆字节转换代码。用国密算法的人还需要下载BouncyCastle的jar包比如bcprov-jdk18on-1.76.jar放到lib/ext。放置jar包后重启JMeter才会生效。怎么确认jar已经被加载最直接的办法新建一个JSR223 Sampler在脚本里写一句log.info(org.apache.commons.codec.digest.DigestUtils.class.getName())执行后去JMeter日志里看有没有报ClassNotFoundException。2.3 小技巧先用一个“裸脚本”验证加密代码可运行很多人一上来就直接写完整的加密预处理器结果逻辑和请求纠缠在一起出了问题很难定位。我每次都是先在JMeter里加一个JSR223 Sampler单独跑加密逻辑把生成的密文打印到log.info或者用vars.put存到变量里然后用Debug Sampler查看。下面这个裸脚本是我最常用的“加密自检”代码验证JMeter环境能否正常跑AESimport javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec import java.util.Base64 String key 1234567890abcdef String iv fedcba9876543210 String plainText hello SecretKeySpec keySpec new SecretKeySpec(key.getBytes(UTF-8), AES) IvParameterSpec ivSpec new IvParameterSpec(iv.getBytes(UTF-8)) Cipher cipher Cipher.getInstance(AES/CBC/PKCS5Padding) cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec) byte[] encrypted cipher.doFinal(plainText.getBytes(UTF-8)) String data Base64.getEncoder().encodeToString(encrypted) log.info(AES encrypted: data) vars.put(encryptedData, data)跑完之后在View Results Tree里看JSR223 Sampler的日志输出能正常打印出一串Base64字符说明环境OK。这一步虽然简单但能帮你把“环境问题”和“业务逻辑问题”迅速分开后面正式写加密接口时心里会踏实很多。3. 实例一AESCBC加密登录接口含签名3.1 接口文档里的加密规则怎么读接到加密接口第一件事不是写代码而是把文档里的加密规则拆解清楚。我拿一个典型的登录接口举例规则通常是这样的业务参数为JSON格式{username:test,password:123456}。用AES算法加密这个JSON字符串加密模式是CBC填充方式是PKCS5Padding密钥是16字节的字符串IV偏移向量也是16字节。加密后的字节数组用Base64编码得到data。将timestamp data secretKey拼成一个字符串再做MD5得到sign。这里有几个容易被忽略的点CBC模式必须有IV而且加解密必须用同一个IVPKCS5Padding在Java里面对应的是块补位16字节密钥对应AES-128密钥的字节长度必须是16、24或者32字节否则会抛异常。看到这些规则你先不用急着理解每个密码学细节只需要知道JMeter发送请求前必须把业务参数按这个流程处理成data和sign。这就是JSR223预处理器的职责。3.2 JSR223预处理器用Groovy在请求前算好密文在JMeter里右键线程组 - 添加 - 前置处理器 - JSR223 PreProcessor。语言选择groovy这是JMeter内置支持的语言性能和可读性都比BeanShell好。然后写入下面的脚本。我一行一行拆开讲import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec import java.util.Base64 import org.apache.commons.codec.digest.DigestUtils // 从CSV或变量中读取明文业务参数 String username vars.get(username) String password vars.get(password) String plainJson {\username\:\ username \,\password\:\ password \} // 固定密钥和IV实际项目中可能配置在属性文件里 String secretKey 1234567890abcdef String iv fedcba9876543210 // AES加密 SecretKeySpec keySpec new SecretKeySpec(secretKey.getBytes(UTF-8), AES) IvParameterSpec ivSpec new IvParameterSpec(iv.getBytes(UTF-8)) Cipher cipher Cipher.getInstance(AES/CBC/PKCS5Padding) cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec) byte[] encryptedBytes cipher.doFinal(plainJson.getBytes(UTF-8)) String data Base64.getEncoder().encodeToString(encryptedBytes) // 生成签名 String timestamp String.valueOf(System.currentTimeMillis()) String signSource timestamp data secretKey String sign DigestUtils.md5Hex(signSource) // 存入JMeter变量供HTTP请求使用 vars.put(timestamp, timestamp) vars.put(data, data) vars.put(sign, sign)这里为什么用vars.get(username)而不是直接用固定值因为测试过程中不同线程、不同用户需要用不同账号把账号参数化到变量里脚本才能复用。System.currentTimeMillis()取的是毫秒级时间戳接口要求秒级还是毫秒级要留意文档。跑一遍这个脚本你会发现data是一长串Base64sign是一串32位的十六进制字符。如果出现Input length must be multiple of 16 when decrypting这类报错通常说明密文或者密钥/IV的字节数不对回到规则里再核对。3.3 HTTP请求里引用加密结果前置处理器执行完timestamp、data、sign三个变量就已经在当前线程作用域里了。HTTP请求的Parameters里直接引用参数名参数值timestamp${timestamp}data${data}sign${sign}请求方法用POST如果接口要求JSON格式可以在Body Data里写成{ timestamp: ${timestamp}, data: ${data}, sign: ${sign} }同时加上HTTP信息头管理器设置Content-Type: application/json;charsetUTF-8。这个charset非常重要后面避坑部分我会再提。执行一次请求如果返回的业务code是0或者登录成功说明加密链路已经跑通。如果还是签名错误优先检查服务端签名时拼接的字段顺序是否和文档一致以及Base64编码是否有换行符。3.4 参数化用户数据让每个线程使用不同账号功能测试只测一个账号远远不够压测更要模拟多用户。JMeter的参数化方式很多我处理加密接口最常用的是CSV数据文件。在测试计划里添加“CSV数据文件设置”配置一个user.csvusername,password test01,123456 test02,123456变量名填username,password。这样JSR223预处理器里的vars.get(username)和vars.get(password)就会自动取到CSV中对应的行数据。有一个细节CSV文件尽量用UTF-8编码保存否则中文用户名或者中文密码在加密前就乱码了后面的加密结果自然也对不上。我自己用Notepad编辑CSV时都会刻意把编码转为UTF-8无BOM格式再保存。4. 实例二RSA非对称加密与签名组合4.1 服务端为什么要用RSA和AES的区别AES加密虽然快但有一个绕不开的问题客户端和服务端必须共享同一个密钥。如果密钥被第三方拿到整个加密体系就形同虚设。RSA解决的是“怎么安全地传递密钥”的问题——客户端用服务端下发的公钥加密数据只有持有私钥的服务端才能解密。所以在实际接口里RSA通常不会去加密大段的业务JSON而是加密“密码字段”或者“AES密钥”。比如登录接口要求把用户密码用服务端公钥加密后传给后端后端用私钥解密拿到明文密码再走内部的认证逻辑。RSA另一个特点是它不能加密太长的数据。用1024位的RSA公钥单次最多加密117字节用2048位的公钥单次最多加密245字节。如果明文超过这个长度必须分段加密。这也是很多测试同学第一次用RSA时最容易被卡住的地方。4.2 从接口响应中提取动态公钥很多业务系统不会把RSA公钥硬编码在前端而是提供一个“获取公钥”接口返回当前可用的公钥字符串。对于这种情况JMeter里需要用关联把公钥提取出来。假设获取公钥的接口响应是{ code: 0, data: { publicKey: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC... } }在获取公钥的HTTP请求下面添加“JSON提取器”Variable namesrsaPublicKeyJSON Path expressions$.data.publicKeyDefault valuesempty这样后续JSR223脚本里就可以用vars.get(rsaPublicKey)拿到公钥字符串。公钥通常是Base64编码的X.509格式Java里需要先解码Base64再交给X509EncodedKeySpec生成PublicKey对象。这个过程不复杂但字符串里的换行符、空格一定要清理干净否则会抛InvalidKeySpecException。4.3 RSA分段加密落地代码下面这段Groovy代码是我在JMeter里处理RSA分段加密的标准模板import javax.crypto.Cipher import java.security.KeyFactory import java.security.PublicKey import java.security.spec.X509EncodedKeySpec import java.util.Base64 String publicKeyStr vars.get(rsaPublicKey).replace(\n, ).replace(\r, ) byte[] keyBytes Base64.getDecoder().decode(publicKeyStr) X509EncodedKeySpec keySpec new X509EncodedKeySpec(keyBytes) KeyFactory keyFactory KeyFactory.getInstance(RSA) PublicKey publicKey keyFactory.generatePublic(keySpec) String plainText vars.get(rsaPlainText) // 要加密的明文比如密码 byte[] inputBytes plainText.getBytes(UTF-8) Cipher cipher Cipher.getInstance(RSA/ECB/PKCS1Padding) cipher.init(Cipher.ENCRYPT_MODE, publicKey) // 分段加密1024位密钥单段117字节2048位密钥单段245字节 int blockSize 117 ByteArrayOutputStream out new ByteArrayOutputStream() for (int i 0; i inputBytes.length; i blockSize) { int len Math.min(blockSize, inputBytes.length - i) out.write(cipher.doFinal(inputBytes, i, len)) } String encryptedBase64 Base64.getEncoder().encodeToString(out.toByteArray()) vars.put(rsaEncrypted, encryptedBase64)这里的blockSize必须根据密钥长度调整1024位密钥是117字节2048位密钥是245字节。如果不分段只要明文超过117字节就会报javax.crypto.IllegalBlockSizeException: Data must not be longer than X bytes。还有一点要牢记同样的明文和公钥每次RSA加密的结果都不一样。因为PKCS1Padding会引入随机数。所以测试时不要拿“两次加密结果完全一致”来验证正确性服务端只要能解密并且验签通过就说明整个链路是通的。4.4 签名串的拼接顺序和坑RSA接口里经常还带签名常见做法是把业务字段按照文档要求的顺序拼接然后用HmacSHA256或者MD5生成签名。这个顺序是接口测试里最容易踩坑的雷区。比如文档要求签名原文是timestamp version body这里的body是加密后的RSA密文。如果你拼接成version timestamp body签名对不上就是必然的。更坑的是有些系统要求参数按ASCII码排序后再拼接比如bodyxxxtimestampxxxversion1.0。我处理过一个项目签名规则是把除sign外的所有参数按key的字典序排列然后拼接成k1v1k2v2的格式最后用商户秘钥做HMACSHA256。代码大致长这样import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec import java.util.Base64 String sortSource body URLEncoder.encode(encryptedBody, UTF-8) sortSource timestamp timestamp sortSource version1.0 Mac mac Mac.getInstance(HmacSHA256) SecretKeySpec sk new SecretKeySpec(secretKey.getBytes(UTF-8), HmacSHA256) mac.init(sk) String sign Base64.getEncoder().encodeToString(mac.doFinal(sortSource.getBytes(UTF-8)))注意这里的URLEncoder.encode。RSA密文是Base64字符串里面可能带、/、这些字符放进URL或者拼接签名原文时服务端解析方式稍有不同签名就会失败。后面避坑部分我还会重点说这个。5. 进阶加密接口的断言、关联与压测5.1 用Beanshell断言解密响应结果接口请求成功后我们不能只看一个HTTP状态码200就完事。如果响应数据也是加密的必须解密后再做业务断言。这时候JMeter的BeanShell断言就能派上用场。在HTTP请求下添加“BeanShell断言”脚本里写解密逻辑import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec import java.util.Base64 String secretKey 1234567890abcdef String iv fedcba9876543210 String response prev.getResponseDataAsString() byte[] encryptedBytes Base64.getDecoder().decode(response) SecretKeySpec keySpec new SecretKeySpec(secretKey.getBytes(UTF-8), AES) IvParameterSpec ivSpec new IvParameterSpec(iv.getBytes(UTF-8)) Cipher cipher Cipher.getInstance(AES/CBC/PKCS5Padding) cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec) String plainText new String(cipher.doFinal(encryptedBytes), UTF-8) log.info(Decrypted response: plainText) if (!plainText.contains(\status\:\success\)) { Failure true FailureMessage 响应解密后未包含成功标识, 解密结果: plainText; }BeanShell里访问断言结果是通过内置变量Failure和FailureMessage。这个方式虽然能用但BeanShell是解释执行的压测时量大就会拖慢线程。所以我在真实项目里更推荐用JSR223 Assertion语言选Groovy脚本思路完全一样性能会好很多。5.2 动态密钥和固定密钥的场景处理有的系统为了安全每次会话都换一个密钥。比如登录接口返回一个sessionKey后续的业务接口都用这个sessionKey加密。这时候就需要把动态密钥关联到JMeter变量里。流程是这样的登录接口返回token和sessionKey。用JSON提取器把sessionKey提取到变量sessionKey。后续业务接口的JSR223预处理器里使用vars.get(sessionKey)作为AES密钥。响应解密同样用这个sessionKey。如果多个线程组要共享同一个动态密钥可以用props.put(sessionKey, sessionKey)和props.get(sessionKey)把密钥提升为JMeter全局属性。vars是线程私有的线程之间不共享props是JVM级别的属性所有线程都能访问。还有一点如果测试计划里有多个业务请求每个请求都要加密那可以把公共的加密逻辑抽到一个Groovy脚本文件里通过JSR223的“脚本文件”选项引用而不是每个前置处理器里都粘贴一遍。这样改密钥算法时只改一个文件维护成本低很多。5.3 压测时加密耗时应如何评估加密接口做性能测试和普通接口有本质区别JMeter不仅要承担HTTP请求的发送还要承担加密计算。如果脚本写得不高效压测结果首先反映的是JMeter所在机器CPU被打满而不是服务端的真实处理能力。这里我给出几个经验值AES加密一个几百字节的JSON耗时在毫秒级以下基本可以忽略但RSA 1024位公钥加密单次耗时约1到2毫秒如果每个请求里多次RSA加密开销就上去了。用BeanShell跑这些脚本耗时会放大数倍所以我强烈建议压测时全部用JSR223 Groovy并且勾选JSR223取样器/前置处理器里的Cache compiled script if available选项。压测策略上先用少量线程比如5个跑一轮观察JMeter所在机器的CPU和内存。如果CPU使用率已经超过80%说明脚本计算消耗过大需要优化脚本或者增加压力机。另外加密接口通常都会有防重放校验比如时间戳不能超过5分钟、同一签名只能使用一次做压测时要注意时间戳的生成时机最好每个请求都在前置处理器里重新生成System.currentTimeMillis()不要整个测试计划共用一个固定时间戳。6. 从踩坑到稳定我的JMeter加密接口测试清单6.1 中文乱码问题编码统一是基础加密接口出现中文乱码是最隐蔽又最折磨人的问题。明文是“张三”加密前如果变成了乱码密文和解密结果自然都是错的而且这种错误只在服务端才能真正发现。我处理这个问题的方法有三层所有JMeter脚本文件、CSV参数文件统一保存为UTF-8编码。在JMeter的jmeter.properties中确认sampleresult.default.encodingUTF-8。在Groovy脚本里字符串转字节时显式写getBytes(UTF-8)不要用无参的getBytes()因为无参会使用系统默认编码Windows中文系统通常就是GBK一换环境就出问题。这三层都做到中文乱码基本能被拦住。6.2 Base64特殊字符在HTTP传输中的处理Base64编码后的字符串可能包含、/、。如果把这个字符串直接放在URL的query参数里HTTP协议会认为代表空格/会被当成路径分隔符服务端解析出来就变了。解决方式有两种对Base64串做URL编码比如用java.net.URLEncoder.encode(base64Str, UTF-8)但URL编码后的%又需要服务端做对应URLDecoder增加了沟通成本。使用URL安全的Base64编码器Base64.getUrlEncoder().withoutPadding()它会把和/替换成-和_并且去掉末尾的。这需要服务端使用对应的URL安全解码器。不管用哪种一定要和服务端确认他们解析密文的方式。我遇到过开发同事说“我用Postman测都没问题”结果一查Postman会自动处理一部分特殊字符而JMeter的HTTP请求不会。这类问题排查起来很费时间最好的办法是一开始就定义好规则。6.3 本地加密结果与服务端不一致的排查路径如果你在JMeter里算出的密文服务端怎么都验签不过别急着重写代码按下面这张表逐项排查排查项检查点密钥和IV字节长度是否符合算法要求密钥是否被转成了Hex或Base64格式填充方式是PKCS5Padding还是PKCS7PaddingAES下两者通常等价但RSA不能混用明文编码字符串转字节时是否统一UTF-8Base64是否带换行是否缺少padding是否用了URL安全模式签名拼接顺序字段顺序是否和文档完全一致有无多余的或空格时间戳接口要求秒级还是毫秒级时区是否一致随机IVAES如果每次都随机生成IV需要把IV拼在密文前传给服务端我自己遇到最多的是签名拼接顺序问题。有一个项目文档写的是“将业务参数按字母顺序排序后拼接”结果我看岔了按英文参数名排的实际上他们要按中文名首字母排。这种问题只能靠开发协助确认但上面的排查表能帮你大大缩小范围。6.4 Beanshell与Groovy的选择性能不只是快一点JMeter里能写脚本的地方都有Beanshell、JSR223 Groovy等选项。Beanshell是JMeter老版本内置的脚本引擎用起来直观网上教程也多搜索引擎里搜“jmeter beanshell断言”能找到一大堆。但Beanshell有两个致命问题解释执行性能慢。一个几百次的循环就能明显感觉到卡顿。某些版本的Beanshell和JMeter的Java类加载方式存在兼容问题调用第三方库时容易出乱七八糟的异常。所以我现在的习惯是能用JSR223就用JSR223语言选Groovy。虽然首次运行Groovy会有一段编译过程但只要勾选了缓存脚本选项后续执行速度非常快写法和Java几乎一样又能用闭包等语法糖。如果你从BeanShell迁到Groovy代码大部分不用改只需要注意log、vars、prev这些内置变量的用法和BeanShell一致。加密接口测试是个需要反复验证的过程脚本不是写出来就完事而是要作为一个长期维护的资产。我在项目里会把加密相关的工具类单独存成一个Groovy文件放进JMeter的bin目录下统一管理每次改了算法只更新这个公共文件所有用到它的前置处理器、断言都会跟着生效。这样不管接口加密规则怎么变测试代码都能快速跟上。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →