JMeter调用JavaScript实现RSA加密接口测试实战指南

📅 2026/7/1 23:10:16
JMeter调用JavaScript实现RSA加密接口测试实战指南
1. 项目概述当JMeter遇上RSA加密在接口测试和性能压测领域JMeter无疑是众多测试工程师和开发者的“瑞士军刀”。它功能强大上手门槛相对友好能模拟各种复杂的HTTP请求场景。然而在实际工作中我们常常会遇到一个棘手的问题接口参数需要加密。特别是像RSA这种非对称加密算法因其安全性高在登录、支付、敏感数据传输等场景中被广泛应用。JMeter本身提供了丰富的内置函数和插件但对于直接调用外部加密算法尤其是需要依赖特定JavaScript库实现的RSA加密就显得有些力不从心了。我最近就遇到了一个真实的项目需求被测系统的登录接口其密码字段要求使用前端JavaScript代码中相同的RSA公钥进行加密后传输。这意味着我必须让JMeter能够“复刻”前端JavaScript的加密行为。如果加密结果对不上服务器就会直接拒绝请求后续的所有测试都无从谈起。手动计算密文再粘贴那显然不现实尤其是在进行压力测试需要生成成千上万个不同用户的请求时。因此“JMeter调用JS文件实现RSA加密”这个需求应运而生。它的核心目标就是打通JMeter与JavaScript加密逻辑之间的壁垒让JMeter能够动态、准确地生成与前端一致的RSA加密密文从而实现对加密接口的自动化与性能测试。这不仅仅是写一个BeanShell脚本那么简单它涉及到JMeter脚本架构设计、JavaScript引擎的选用、加密库的兼容性以及调试排错等一系列实战问题。接下来我将详细拆解整个实现过程分享其中踩过的坑和总结出的最佳实践。2. 核心思路与方案选型面对“JMeter执行JS加密”这个问题我们首先需要明确几个关键点加密逻辑在哪里JMeter如何执行JS如何保证结果一致2.1 加密逻辑的来源与分析通常RSA加密逻辑来源于前端代码。你需要做的第一步是“借用”前端的加密代码。打开浏览器的开发者工具F12找到登录页面对应的JavaScript文件搜索encrypt、RSA、publicKey等关键词。你很可能会找到类似这样的代码片段它可能使用了jsencrypt、node-rsa或crypto-js等库。例如一个典型的jsencrypt加密片段// 前端加密示例 var encrypt new JSEncrypt(); encrypt.setPublicKey(-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSq...\n-----END PUBLIC KEY-----); var encrypted encrypt.encrypt(myPassword123); console.log(encrypted);我们的目标就是将包含类似逻辑的整个函数或者整个JS文件移植到JMeter中能够被执行的环境里。这里要注意前端代码可能经过压缩、混淆或者依赖了浏览器特有的对象如window、document直接复制过来可能无法运行需要做适当的净化和适配。2.2 JMeter执行JS的几种方案对比JMeter本身是Java应用它提供了几种方式来执行外部脚本或代码我们需要从中选择最合适的一种。方案一BeanShell Sampler/PreProcessor这是JMeter内置的脚本支持使用BeanShell一种Java语法风格的脚本语言。理论上你可以通过BeanShell调用Java的ScriptEngine来执行JavaScript。但这条路坑很多需要处理JDK版本与JS引擎如Nashorn的兼容性问题且Nashorn在较新的JDK中已被标记为废弃对于复杂JS库的支持并不理想。不推荐作为首选。方案二JSR223 Sampler/PreProcessor这是更强大、更现代的选择。JSR223是Java平台的标准脚本API支持包括Groovy、JavaScriptNashorn、PythonJython等多种语言。关键在于Groovy脚本在JMeter中性能极佳且与Java无缝集成。我们的思路是在JSR223元件中使用Groovy脚本来调用并执行一个独立的JavaScript文件。Groovy可以很方便地操作文件、调用Java的脚本引擎或者直接集成一个轻量级的JS解释器。方案三使用“OS Process Sampler”调用Node.js这是一种“曲线救国”但非常稳定的方法。即写一个简单的Node.js脚本接收JMeter传递的参数调用JS加密库进行加密然后将结果输出。JMeter通过OS Process Sampler执行这个Node.js命令并捕获其标准输出作为加密结果。这种方法隔离性好能100%复用前端的Node.js环境但会引入额外的进程开销在超高并发压测时需谨慎。方案四纯Java实现如果加密算法是标准的且公钥固定最彻底的方式是直接用Java代码实现相同的RSA加密。你可以将找到的JS加密库的算法逻辑用Java的java.security包重写。这种方式性能最好但实现成本高且当前端加密逻辑变更时需要同步更新Java代码。综合考量实现难度、稳定性、可维护性和性能对于大多数测试场景方案二JSR223 Groovy 调用 JS是平衡性最好的选择。它既能灵活处理JS代码又避免了进程调用的开销且Groovy脚本易于调试和集成到JMeter变量体系中。本篇文章也将重点围绕这个方案展开。3. 环境准备与核心工具解析在开始动手之前我们需要搭建好工作环境并理解几个核心“工具”的工作原理。3.1 JMeter与JDK环境确认首先确保你的JMeter建议使用5.4.1及以上版本运行在合适的JDK上。由于我们需要用到javax.script包JSR223的核心JDK 8是一个广泛兼容的选择它内置了Nashorn JavaScript引擎。如果你使用JDK 11或更高版本Nashorn可能被移除这时就需要考虑其他JS引擎比如GraalVM JavaScript。注意JMeter 5.5 版本对高版本JDK的支持更好。如果你计划使用GraalVM JS需要将相应的jar包如graal-sdk.jar,truffle-api.jar,graal-js.jar放入JMeter的lib目录下。为了简化本文先以仍支持Nashorn的JDK 8环境为例。3.2 加密JS文件的获取与处理从前端获取的JS文件可能是一个混合了多种功能的单一文件。我们需要从中剥离出加密所需的最小代码集合。通常包括两部分RSA加密库的核心代码例如jsencrypt.js的源码。调用加密库的包装函数一个我们自定义的、接收明文返回密文的函数。处理步骤保存一个纯净的jsencrypt.js文件到本地例如从https://cdnjs.cloudflare.com/ajax/libs/jsencrypt/3.0.0/jsencrypt.min.js下载。创建一个新的JS文件比如叫做rsa_encryptor.js。在这个文件里我们先引入或直接粘贴jsencrypt.js的源码然后编写我们的包装函数。// rsa_encryptor.js // 1. 此处粘贴完整的 jsencrypt.js 源码 // 或者如果JMeter环境能加载外部文件也可以分开 // 2. 定义加密函数 function encryptPassword(plainText) { // 公钥必须与前端完全一致注意换行符 var publicKey -----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC...你的公钥内容... -----END PUBLIC KEY-----; var encryptor new JSEncrypt(); encryptor.setPublicKey(publicKey); var encrypted encryptor.encrypt(plainText); // 返回加密后的字符串 return encrypted; } // 3. 提供一个供外部调用的接口 // 在JMeter中我们可以通过脚本引擎的 eval 执行整个文件然后调用这个函数。实操心得公钥字符串中的换行符\n至关重要。很多加密失败的情况都是因为公钥格式不正确。建议将前端使用的公钥原封不动地复制过来使用反引号模板字符串可以很好地保持格式。另外检查公钥是否包含-----BEGIN PUBLIC KEY----- 这样的头尾标记有些实现需要有些不需要。3.3 JSR223 Sampler与Groovy脚本基础JSR223元件是连接JMeter与外部脚本的桥梁。在JMeter线程组中添加一个JSR223 Sampler或JSR223 PreProcessor通常PreProcessor更常用用于在发送请求前处理参数。在JSR223元件中语言选择选择groovy。Groovy在JMeter中编译后会被缓存性能远好于其他脚本语言。脚本编写区域我们将在这里编写Groovy代码来读取并执行上面的rsa_encryptor.js文件。核心的Groovy代码如下框架import javax.script.ScriptEngine import javax.script.ScriptEngineManager // 1. 获取JavaScript脚本引擎 ScriptEngineManager factory new ScriptEngineManager() ScriptEngine engine factory.getEngineByName(nashorn) // 或 graal.js // 2. 读取JS文件内容 String jsFilePath 你的路径/rsa_encryptor.js File jsFile new File(jsFilePath) String jsCode jsFile.text // 3. 执行JS代码将加密函数加载到引擎中 engine.eval(jsCode) // 4. 调用JS中的加密函数 String plainPassword vars.get(plain_password) // 从JMeter变量中获取明文密码 // 调用JS函数注意nashorn的调用方式 def encryptedPassword engine.invokeFunction(encryptPassword, plainPassword) // 5. 将加密结果存回JMeter变量供HTTP请求使用 vars.put(encrypted_password, encryptedPassword as String) // 可选打印日志便于调试 log.info(明文: plainPassword - 密文: vars.get(encrypted_password))4. 完整实现步骤详解现在我们将所有环节串联起来构建一个可工作的JMeter测试计划。4.1 步骤一构建测试计划骨架创建线程组设置你需要的线程数、循环次数等。创建HTTP请求默认值可选配置协议、服务器IP、端口等公共信息。创建登录HTTP请求先不填参数地址为/api/login方法为POST。4.2 步骤二实现动态加密逻辑这是最核心的一步。我们不会在HTTP请求中直接写死密码而是通过前置处理器来动态生成。添加用户定义的变量可选但推荐添加一个变量plain_password值为test123。这样便于管理和修改。添加JSR223 PreProcessor右键点击登录HTTP请求 - 添加 - 前置处理器 - JSR223 PreProcessor。语言选择groovy将 [3.3] 小节中的Groovy脚本复制到脚本区域。关键修改将你的路径/rsa_encryptor.js替换为你的JS文件绝对路径如C:/JMeterScripts/rsa_encryptor.js。更佳实践是使用相对路径并将JS文件放在JMeter脚本.jmx同一目录下使用${__P(user.dir)}来获取当前目录。String jsFilePath ${__P(user.dir)}/rsa_encryptor.js确保engine.getEngineByName(nashorn)与你的JDK环境匹配。4.3 步骤三配置HTTP请求参数回到登录HTTP请求配置Body Data如果接口接收JSON或Parameters。假设接口需要JSON{username: testUser, password: ${encrypted_password}}在“消息体数据”中填入上述JSON。JMeter会在执行时将${encrypted_password}替换为JSR223 PreProcessor计算出的密文。4.4 步骤四添加断言与调试元件为了验证加密和请求是否成功必须添加断言。添加响应断言检查响应体是否包含登录成功的特征如code: 200或success: true。添加调试取样器和查看结果树在开发调试阶段极其有用。调试取样器会打印出所有JMeter变量的值查看结果树可以查看请求和响应的详细信息。4.5 步骤五运行与验证点击运行。在“查看结果树”中检查发送出去的请求。重点查看请求体中的password字段是否已经变成了一长串Base64编码格式的密文RSA加密后的典型输出。检查响应断言是否通过。可以将JMeter生成的密文与直接使用浏览器开发者工具Console执行相同加密函数得到的密文进行对比确保完全一致。5. 高级技巧与性能优化基础功能实现后我们还需要关注效率、稳定性和可维护性。5.1 加密结果缓存策略在性能测试中如果每个虚拟用户、每次循环都重新加载JS文件、初始化加密对象会造成巨大的资源浪费。我们可以利用JMeter的变量作用域和Groovy脚本的灵活性来实现缓存。思路将初始化好的ScriptEngine和加密函数对象缓存到JMeterContext或JMeterVariables中。但更优雅的方式是利用JSR223 PreProcessor的“缓存编译后的脚本”功能。在JSR223元件的底部有一个复选框“Cache compiled script if available”。务必勾选。这会让JMeter缓存编译后的Groovy脚本极大提升性能。对于JS引擎本身我们可以在脚本中做判断只初始化一次。修改后的脚本框架如下import javax.script.ScriptEngine import javax.script.ScriptEngineManager // 尝试从线程局部变量中获取引擎 ScriptEngine engine vars.getObject(jsEngine) if (engine null) { log.info(初始化JavaScript引擎...) ScriptEngineManager factory new ScriptEngineManager() engine factory.getEngineByName(nashorn) // 读取并执行JS文件 String jsCode new File(${__P(user.dir)}/rsa_encryptor.js).text engine.eval(jsCode) // 将引擎对象存入变量供同一线程后续迭代使用 vars.putObject(jsEngine, engine) } else { log.debug(复用已缓存的JavaScript引擎。) } // 调用加密函数 String plainPassword vars.get(plain_password) try { def encryptedPassword engine.invokeFunction(encryptPassword, plainPassword) vars.put(encrypted_password, encryptedPassword as String) } catch (Exception e) { log.error(加密调用失败: , e) vars.put(encrypted_password, ENCRYPTION_ERROR) }注意事项vars.putObject和vars.getObject用于存储和获取非字符串对象如ScriptEngine。这种缓存方式在同一线程内有效。不同线程不同的虚拟用户会各自创建自己的引擎实例这是线程安全的。5.2 处理更复杂的JS加密场景有时前端加密并非一个简单的函数调用可能涉及多个步骤比如先对字符串做哈希MD5/SHA再进行RSA加密或者需要生成随机数作为盐值。应对策略完整移植将前端整个加密流程的JS函数全部封装到你的rsa_encryptor.js中。提供一个统一的入口函数如function fullEncrypt(data)。环境模拟如果JS代码依赖window、document或btoa/atob等浏览器对象你需要在JS文件开头或Groovy执行前模拟这些对象。// 在eval JS代码之前注入模拟的window对象 engine.eval(var window this; var document {};) // 如果JS代码使用了btoa可以注入polyfill engine.eval( if (typeof btoa undefined) { var btoa function(str) { return java.util.Base64.getEncoder().encodeToString(str.bytes); }; } )使用GraalVM JS引擎对于非常复杂、现代的前端JS代码ES6Nashorn可能无法解析。切换到GraalVM JS引擎是更好的选择。你需要下载GraalVM JS的jar包并放入lib目录然后将脚本引擎名称改为graal.js。5.3 将配置外部化将公钥、JS文件路径等配置信息硬编码在脚本里不利于维护。最佳实践是将其外部化。使用JMeter属性Properties在jmeter.properties或user.properties文件中定义rsa.public.key.path/config/public_key.pem rsa.js.file.path/scripts/encryptor.js在Groovy脚本中读取String publicKeyPath props.get(rsa.public.key.path) String jsFilePath props.get(rsa.js.file.path)使用CSV数据文件如果不同用户需要使用不同的密钥可以将密钥作为测试数据和用户名、密码一起放在CSV文件中通过CSV Data Set Config读取。6. 常见问题排查与调试实录即使按照步骤操作你也可能会遇到各种问题。下面是我在实战中遇到的一些典型情况及其解决方法。6.1 加密结果为空null或错误这是最常见的问题。检查公钥格式这是头号嫌疑犯。确保公钥字符串与前端完全一致包括头尾标记和换行符。可以将公钥字符串打印到日志中仔细比对。log.info(使用的公钥: publicKeyString)检查JS引擎执行是否报错将engine.eval(jsCode)用try-catch包裹打印异常信息。try { engine.eval(jsCode); } catch (Exception e) { log.error(JS代码执行失败: , e); }检查JS函数是否存在在调用invokeFunction前可以尝试评估一个测试表达式。engine.eval(console.log(typeof encryptPassword);) // Nashorn下console.log需要模拟或使用print // 或者 def func engine.get(encryptPassword) log.info(加密函数对象: func)明文内容问题确保传递给JS函数的明文密码是字符串类型且没有不可见字符。6.2 性能低下TPS不达标确认缓存已开启检查JSR223元件的“缓存编译后的脚本”是否勾选。避免每次迭代都读文件使用 [5.1] 小节的引擎缓存策略确保JS文件只被加载和解析一次。减少日志输出在正式压测时将log.info改为log.debug或移除避免I/O成为瓶颈。检查JS代码本身有些前端JS加密库为了兼容性做了很多判断可能比较重。可以考虑寻找或编写一个更轻量、功能单一的RSA加密JS实现。6.3 在高版本JDK如JDK 11中Nashorn不可用方案A降级到JDK 8。最简单直接。方案B使用GraalVM JavaScript引擎。从GraalVM官网下载对应版本的GraalVM JDK或者单独下载graal-js的jar包。将graal-sdk.jar,truffle-api.jar,graal-js.jar等必要jar包复制到JMeter的lib目录。将Groovy脚本中的引擎名称改为graal.js。注意GraalVM JS的上下文可能与Nashorn略有不同可能需要调整模拟全局对象的代码。6.4 响应断言失败但请求已发送首先确认加密是否正确使用“查看结果树”查看请求体复制出密文。然后打开浏览器开发者工具的Console手动执行前端加密函数对同一个密码进行加密对比两个密文是否完全一致。如果不一致回到上一步排查。检查请求格式确认HTTP请求的Content-Type头是否正确如application/json。确认JSON格式是否正确密码字段的引号是否完整。查看服务器响应在“查看结果树”中查看服务器的原始响应可能错误信息就在里面。可能是公钥不匹配、加密算法不对、或者是其他业务参数错误。6.5 问题排查速查表问题现象可能原因排查步骤密文为null1. JS引擎执行错误2. 公钥格式错误3. 加密函数未定义或调用失败1. 用try-catch包裹eval并打印日志2. 打印并比对公钥字符串3. 检查JS函数名用engine.get()验证密文与前端不一致1. 公钥不同2. 明文被意外修改空格、编码3. JS加密逻辑不一致如填充方式1. 严格比对公钥2. 打印明文日志比对3. 确认使用的JS库版本和前端一致JMeter报错No such function1. JS文件未成功加载2. 函数名拼写错误3. JS代码有语法错误导致函数未定义1. 检查JS文件路径和内容2. 在eval后执行print(Object.keys(this))查看全局对象性能极差1. 脚本缓存未开启2. 每次迭代都读取文件3. 日志级别过高1. 勾选缓存选项2. 实现引擎对象缓存3. 调整log级别为WARN或ERRORJDK 11 下脚本不工作Nashorn引擎缺失1. 切换至JDK 82. 或引入GraalVM JS引擎7. 总结与扩展思考通过以上步骤我们成功地在JMeter中构建了一个能够动态调用JavaScript进行RSA加密的测试方案。这个方案的核心在于JSR223 PreProcessor Groovy JavaScript引擎的组合它平衡了灵活性、性能和实现复杂度。回顾整个过程有几个关键点值得再次强调公钥一致性这是成功的基石务必一丝不苟地复制。脚本缓存无论是Groovy脚本还是JS引擎缓存是性能测试的生命线。调试先行充分利用“调试取样器”和“查看结果树”在开发阶段就验证每一步的输出比盲目压测高效得多。这个模式具有很强的扩展性。它不仅仅适用于RSA加密任何前端使用的、JMeter不易直接实现的加密算法如国密SM2、SM4或者自定义的混淆算法都可以通过这种方式“移植”过来。你甚至可以用它来处理复杂的请求签名逻辑比如需要将多个参数按特定规则排序、拼接、再哈希的签名算法。更进一步你可以将这个加密逻辑封装成一个JMeter自定义函数这样就能像使用__MD5一样在参数中直接使用__RSAEncrypt函数使得测试脚本更加简洁和可读。这需要你编写一个Java类实现JMeter的Function接口并在其中集成我们上面讨论的Groovy和JS引擎调用逻辑最后打包成jar包放入JMeter的lib/ext目录。这算是这个技术的“终极形态”了对于团队内需要频繁测试加密接口的场景能极大提升效率。最后技术选型永远服务于测试目标。如果接口相对稳定且对性能要求极高最终转向纯Java实现仍然是值得考虑的。但对于快速验证、应对频繁变化的加密逻辑本文所详述的JS调用方案无疑是测试工程师手中一把锋利而趁手的“万能钥匙”。