尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

JMeter接口加密参数实战:从sign签名到国密算法全解析

发布时间:2026/9/24 19:19:33

资讯中心
01
ARTICLE

JMeter接口加密参数实战:从sign签名到国密算法全解析

JMeter接口加密参数实战:从sign签名到国密算法全解析
这周最烦的一件事是压测环境里所有请求突然开始报 sign 校验失败。开发那边给的说法很统一安全要求所有接口的请求参数必须带上加密签名。Jmeter 脚本里原来的参数直接暴露在请求里现在必须把加密参数动态生成、动态塞进请求整个过程折腾了好几天。其实Jmeter 请求发送加密参数这件事本身不复杂但前提是你得先搞清楚接口到底要的是哪种加密。我做过的项目里有只加一个 sign 签名的有把敏感字段用 AES 加密的还有一套系统直接上了国密 SM2/SM3/SM4三种情况在 JMeter 里的处理方式差别非常大。这篇文章我就按实际的排查顺序把从接口开始报签名错误到压测脚本稳定跑起来这整条链路写清楚适合正在被接口加密折腾的测试开发、测试工程师和后端联调人员。1. 先分清接口要的加密参数到底是哪一种1.1 整体加密、字段加密、参数签名是三件不同的事很多人一看到加密参数四个字就以为是要写一堆加密代码其实第一步是判断加密形态。我见过至少有三种参数签名请求体里多出一个sign或signature字段它的值是其他参数加上密钥做摘要计算的结果。服务端收到请求后用同样的规则重新算一遍对不上就拒绝。字段加密请求体里某个或某几个字段的值是密文比如手机号、身份证号、银行卡号这类敏感信息明文不可读通常是一串 Base64 或十六进制字符串。整体加密整个请求体被加密成一个字符串外层用一个固定字段比如data或message包裹明文参数完全看不到。用一个生活化的比喻签名相当于快递单上的收件人签字证明这个包裹是本人确认过的字段加密相当于快递箱里贵重物品单独上了把锁盒子本身还是透明的整体加密相当于把整个箱子焊死只有拿到钥匙的人才看得到里面是什么。这三种形态在 JMeter 里的处理思路完全不一样。签名场景你只需要在请求发送前加一个 PreProcessor计算完把 sign 塞进参数里就行字段加密场景需要把原本要放明文的地方替换成加密后的值整体加密场景整个 HTTP Body 都要替换成密文字符串而且几乎百分百要配合 BeanShell 或 JSR223 脚本因为 JMeter 自带的函数很难完成这么复杂的逻辑。1.2 从抓包和接口文档快速判断加密类型判断方法其实很直观两步就够。第一步抓包看请求体。如果还是明文 JSON只是多了一个sign字段那就是参数签名如果请求体里的关键字段是一串无意义的乱码那就是字段加密如果整个 body 只有一个密文串那是整体加密。第二步看接口文档或找开发确认。文档里通常会说明加密算法、密钥、字符集、签名规则。如果文档没有就直接看服务端报错报 签名校验失败 十有八九是签名逻辑问题报 数据解密失败 那是字段或报文加密的问题。我整理了一张对照表方便你快速对号入座加密形态请求示例主要特征JMeter 处理策略参数签名{name:test,timestamp:1710000000,sign:a3f...}有 sign/signature 字段其他参数可读JSR223 预处理器动态生成 sign字段加密{phone:U2FsdGVkX13cNf...}敏感字段密文结构仍可读对指定字段做加密后传参整体加密{data:sWfLqP2nFQDx...}Body 是整段密文无明文参数构造完整加密报文后替换 HTTP Body还有一种更容易被忽略的情况加密参数不只是放在 body 里也可能放在 URL 的 query string 或请求头里。比如有些接口要求X-Sign请求头、timestamp在 URL 上、nonce在 body 里。这时候千万别只盯着 body 看JMeter 里可能需要同时配置 HTTP Header Manager、HTTP Request 的 Parameters 和 Body Data。1.3 加密参数在请求里的位置决定了用哪个组件同一种加密算法参数位置不同脚本挂载的位置也不一样。我的习惯是只影响某个 HTTP 请求的加密参数直接挂在那个请求下的 JSR223 PreProcessor作用域最小不会误伤其他请求。同一线程组内多个请求都要用同一个加密逻辑挂在线程组下的 JSR223 PreProcessor 或者用 setUp Thread Group 生成全局变量。加密参数要加到请求头就在脚本里用vars.put(sign, sign)然后在 HTTP Header Manager 里引用${sign}。这里要特别注意JSR223 PreProcessor 的执行顺序是在 Sampler 发送请求之前所以你在里面生成的变量当前请求就能引用。但如果把加密逻辑放在 PostProcessor那算出来的值只能给下一个请求用方向完全搞反了。2. 环境准备JSR223 预处理器 Groovy而不是 BeanShell2.1 为什么我强烈推荐 JSR223 Groovy很多老教程还在教用 BeanShell 写加密脚本但 JMeter 3.x 之后的官方文档明确不建议在压测场景用 BeanShell原因有两个一是 BeanShell 解释执行的性能差高并发下会拖垮线程二是 BeanShell 对 Java 新语法支持不完整遇到 Lambda、泛型这些容易出问题。JSR223 Groovy 是目前最稳妥的组合。Groovy 完全兼容 Java 语法写加密算法基本就是把 Java 代码粘过来改改就能跑而且 JSR223 引擎在 JMeter 里会缓存编译结果性能比 BeanShell 高一个数量级。我在压测环境用 Groovy 跑 RSA 加密单请求加密耗时在毫秒级线程多了也没有明显抖动。踩过的另一个坑是脚本语言写成 BeanShell 后一旦遇到高并发压测CPU 会飙升得很厉害TPS 上不去最后排查下来发现瓶颈根本不是被测系统而是 JMeter 自己在用解释器跑加密逻辑。换成 Groovy 之后同样一台压测机单机线程数还能再往上加。2.2 第三方加密库怎么装以 BouncyCastle 为例做常规的 MD5、SHA、AES、RSA 不需要额外装库JDK 自带的javax.crypto和java.security就够。但如果系统上了国密算法比如 SM2、SM3、SM4JDK 默认是不支持的必须引入 BouncyCastle通常简称为 BC 库。安装流程去 Maven 中央仓库搜索bcprov-jdk18on下载最新版 jar 包。把 jar 包扔到 JMeter 安装目录的lib/ext文件夹下。完全停止 JMeter再重新启动。注意是放到lib/ext不是lib。放错地方的话JMeter 启动时也能加载但 JSR223 脚本里不一定能正确 import。我第一次就是放到了lib目录结果脚本里import org.bouncycastle.jce.provider.BouncyCastleProvider一直报 ClassNotFound后来才发现是目录问题。如果你用的是 Maven 项目也可以在工程里引入依赖后打成 fat jar再丢到lib/ext效果一样dependency groupIdorg.bouncycastle/groupId artifactIdbcprov-jdk18on/artifactId version1.78/version /dependency2.3 一个最小可用的 Groovy 加密脚本骨架在动手写具体算法之前我建议你先在 JMeter 里放一个最基础的 Groovy 预处理器验证环境没问题再往上叠加逻辑。骨架如下import java.security.MessageDigest // 1. 从 JMeter 变量里读取业务参数 String appId vars.get(appId) String timestamp String.valueOf(System.currentTimeMillis()) // 2. 组装待签名字符串 String raw appId appId timestamp timestamp key你的密钥 // 3. 计算 MD5 MessageDigest md MessageDigest.getInstance(MD5) byte[] digest md.digest(raw.getBytes(UTF-8)) StringBuilder sb new StringBuilder() digest.each { byte b - sb.append(String.format(%02x, b)) } // 4. 存回 JMeter 变量供 HTTP 请求引用 vars.put(sign, sb.toString()) vars.put(timestamp, timestamp)这段脚本本身很简陋但骨架是对的。它能确认三件事JSR223 脚本能正常执行、Groovy 能 import Java 类、vars.put的变量能被 HTTP 请求取到。后面所有的加密逻辑都是在这个骨架里加代码。3. 三种高频加密场景的脚本模板3.1 sign 签名MD5/HmacSHA256先排序后拼接参数签名是遇到最多的场景八成以上接口的 sign 都是这种套路。典型规则是把业务参数按参数名升序排列排除空值和签名本身拼成key1value1key2value2格式最后拼接密钥再做 MD5、SHA-256 或 HmacSHA256。直接给一个可以在 JSR223 里跑的完整模板假设业务参数有name、amount密钥是abc123import java.security.MessageDigest import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec // 从 JMeter 变量取参数如果没有就用默认值 String name vars.get(name) ?: test String amount vars.get(amount) ?: 100 // 生成时间戳和随机数 String timestamp String.valueOf(System.currentTimeMillis()) String nonce UUID.randomUUID().toString().replace(-, ) // 用 TreeMap 保证参数按 key 升序 TreeMapString, String params new TreeMap() params.put(name, name) params.put(amount, amount) params.put(timestamp, timestamp) params.put(nonce, nonce) // 拼接待签名串 StringBuilder sb new StringBuilder() params.each { k, v - if (v ! null v.length() 0) { sb.append(k).append().append(v).append() } } String raw sb.toString() keyabc123 // 计算 HmacSHA256 Mac mac Mac.getInstance(HmacSHA256) SecretKeySpec keySpec new SecretKeySpec(abc123.getBytes(UTF-8), HmacSHA256) mac.init(keySpec) byte[] result mac.doFinal(raw.getBytes(UTF-8)) String sign result.collect { String.format(%02x, it) }.join() // 写回 JMeter 变量 vars.put(timestamp, timestamp) vars.put(nonce, nonce) vars.put(sign, sign)这里有个细节容易被忽略raw字符串的编码。开发那边如果用的 UTF-8你这里就必须指定getBytes(UTF-8)否则一旦参数里有中文签名大概率对不上。另外拼接顺序也很关键必须严格按照开发文档来是排除空值再拼接还是空值也拼接是最后拼密钥还是最前面拼密钥每家的规则都不一样。最好的方式就是把开发写的 Java 签名工具类原文要过来照着翻译成 Groovy。如果开发给的是 MD5把Mac那几行换成MessageDigest.getInstance(MD5)就行前面的拼接逻辑完全复用。3.2 AES 加密字段CBC 模式下的密钥与 IV字段加密最常见的算法是 AES。AES 有很多模式ECB、CBC、GCM实际项目里我用得最多的是 CBC PKCS5Padding因为大多数后端框架默认就是这套组合。假设接口要求把phone字段加密后传参密钥 16 字节IV 固定为 16 字节代码如下import javax.crypto.Cipher import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.IvParameterSpec String phone vars.get(phone) ?: 13800138000 String key 0123456789abcdef // 16位密钥 String iv fedcba9876543210 // 16位IV byte[] keyBytes key.getBytes(UTF-8) byte[] ivBytes iv.getBytes(UTF-8) Cipher cipher Cipher.getInstance(AES/CBC/PKCS5Padding) SecretKeySpec keySpec new SecretKeySpec(keyBytes, AES) IvParameterSpec ivSpec new IvParameterSpec(ivBytes) cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec) byte[] encrypted cipher.doFinal(phone.getBytes(UTF-8)) // 用 Base64 还是 Hex看接口要求 String phoneEnc encrypted.encodeBase64().toString() // 如果是 hex用下面这行 // String phoneEnc encrypted.collect { String.format(%02x, it) }.join() vars.put(phone, phoneEnc)用 Base64 还是 Hex接口文档一定会写别自己猜。Base64 的结果里可能出现、/、如果这个加密字段还要拼到 URL 参数里必须做 URL 编码否则服务端解析出来的值会被截断或变成空格。最保险的做法是在 HTTP Request 的 Parameter 里勾选Encode?或者自己在脚本里用URLEncoder.encode(phoneEnc, UTF-8)。密钥和 IV 千万不要硬编码在 jmx 文件里最后传给别人的时候带着。我一般从 CSV 文件或 JMeter 属性里读取既方便多环境切换也不会把生产密钥留在脚本里。3.3 RSA 非对称加密适合少量敏感数据RSA 的特点是慢但安全性高适合加密少量数据比如登录密码。接口里常见的用法是前端用公钥加密密码后端用私钥解密。import javax.crypto.Cipher import java.security.KeyFactory import java.security.spec.X509EncodedKeySpec import java.util.Base64 String publicKeyStr vars.get(publicKey) ?: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC... String password vars.get(password) ?: 123456 byte[] keyBytes Base64.getDecoder().decode(publicKeyStr) X509EncodedKeySpec spec new X509EncodedKeySpec(keyBytes) KeyFactory factory KeyFactory.getInstance(RSA) Cipher cipher Cipher.getInstance(RSA/ECB/PKCS1Padding) cipher.init(Cipher.ENCRYPT_MODE, factory.generatePublic(spec)) byte[] encrypted cipher.doFinal(password.getBytes(UTF-8)) String passwordEnc Base64.getEncoder().encodeToString(encrypted) vars.put(password, passwordEnc)这里容易出现的问题是公钥格式。有的后端给的是-----BEGIN PUBLIC KEY-----包裹的 PEM 格式需要去掉头和尾还得把换行去掉再 Base64 解码。我一般直接找开发要裸的 Base64 字符串省去解析 PEM 的麻烦。3.4 国密 SM2/SM3/SM4用 BouncyCastle 落地现在越来越多政企、金融、电力系统要求使用国密算法JMeter 里真正用起来也没多难核心就是引入 BouncyCastle 库然后调用对应算法。先注册 Provider再调用。举 SM3 摘要为例import org.bouncycastle.jce.provider.BouncyCastleProvider import java.security.MessageDigest import java.security.Security Security.addProvider(new BouncyCastleProvider()) String raw appIdtesttimestamp1710000000key密钥 MessageDigest digest MessageDigest.getInstance(SM3, BC) byte[] hash digest.digest(raw.getBytes(UTF-8)) String sm3Hex hash.collect { String.format(%02x, it) }.join() vars.put(sign, sm3Hex)SM4 对称加密的写法和 AES 几乎一样只是算法名换成SM4/CBC/PKCS5PaddingSecurity.addProvider(new BouncyCastleProvider()) String data vars.get(plainText) ?: hello String key 1234567890abcdef String iv abcdef1234567890 Cipher cipher Cipher.getInstance(SM4/CBC/PKCS5Padding, BC) SecretKeySpec keySpec new SecretKeySpec(key.getBytes(UTF-8), SM4) IvParameterSpec ivSpec new IvParameterSpec(iv.getBytes(UTF-8)) cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec) byte[] encrypted cipher.doFinal(data.getBytes(UTF-8)) String encryptedStr encrypted.collect { String.format(%02x, it) }.join() vars.put(encryptedData, encryptedStr)SM2 非对称加密和签名稍微复杂一点因为涉及密钥对、C1C3C2 或 C1C2C3 的排列顺序。这里不展开所有细节给一个 SM2 签名的核心写法Security.addProvider(new BouncyCastleProvider()) String data 待签名字符串 // 假设已经从文件或变量里拿到了私钥 org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey privateKey ... java.security.Signature signature java.security.Signature.getInstance(SM3withSM2, BC) signature.initSign(privateKey) signature.update(data.getBytes(UTF-8)) byte[] signBytes signature.sign() String signHex signBytes.collect { String.format(%02x, it) }.join()实际做国密接口时最耗时间的不是写脚本而是和开发对齐密钥格式和排列规则。开发那边如果是用 Java 的 BouncyCastle 写的那你这边基本可以照着抄如果开发用的是 C 语言或其他语言实现就要格外小心 SM2 密文的排列方式因为 C 的常见实现和 Java 的 BC 库可能在C1C3C2/C1C2C3上不一致导致联调时加密出来的数据服务端解不开。4. 调试和压测中最容易踩的坑4.1 参数值里的空格、空值、类型对签名的影响签名的规则往往是很苛刻的。开发那边如果判断空值不参与签名你脚本里就必须排除如果判断空值参与签名你也要跟着拼一个空的key上去。最怕的是参数值前后有空格比如name的值从 CSV 文件里读出来时带了一个换行或空格签名怎么算都不对。还有一个隐藏坑是数字类型的一致性。接口文档说amount100但那是一个整数开发在签名时可能用的是100字符串如果 CSV 里存的是100.0或者脚本里用了amount.toString()得到100两边就不一致了。这种问题只能在调试时打日志对比。我的排查套路是先在 JSR223 脚本最后面用log.info(raw{ raw })把待签名字符串打印到 jmeter.log再让开发把他收到的签名串也打出来两个一对比哪里多了空格、哪里少了个一目了然。4.2 UTF-8 与 URL 编码是两码事签名计算用的编码和 HTTP 传输用的编码不是同一个概念。签名用的raw.getBytes(UTF-8)是把字符串转字节URL 编码是把特殊字符转成%xx形式。这两步经常被人搞混。如果参数里有中文签名前是原样中文还是先用 URLEncoder 编码完全取决于开发实现。有些开发是先URLEncoder.encode再参与签名有些是直接拼接再对整串做摘要。不要想当然一定要看开发代码。Base64 出来的字符串更麻烦可能包含、/、。如果这个字段出现在 URL query 里会被服务端解析成空格导致解密结果都不一样。这种情况我一般会用URLEncoder.encode(encrypted, UTF-8)包一层或者用 URL 安全的 Base64把换成-把/换成_去掉。4.3 时间戳、随机数在并发压测时的取值时机这是个经典的坑。我之前在一个脚本里写过timestamp${__time(,)}HTTP 请求的 Body 里放了一个${__time(,)}然后在 JSR223 预处理器里又用了System.currentTimeMillis()生成签名。看起来没啥问题实际跑起来签名校验时好时坏。原因很简单__time()是一个函数每次被引用时都会重新执行。Body 里的${__time(,)}和签名里的时间戳虽然是同一毫秒级时刻取到的值也可能不一样只要有一毫秒的差值服务端对比时间戳就可能失败。正确的做法是在 JSR223 脚本里只生成一次时间戳String timestamp String.valueOf(System.currentTimeMillis()) vars.put(timestamp, timestamp)后面请求体里一律用${timestamp}引用变量而不是再次调用__time()。随机数也是同理。如果在签名里用了UUID.randomUUID()又在请求体里用${__UUID()}生成了另一个随机数服务端收到的 nonce 和验签时的 nonce 就会不一致。整个请求里所有需要复用的值都应该只在脚本里生成一次然后通过变量共享。4.4 加密逻辑对压测机性能的影响加密计算是有 CPU 开销的。MD5 和 SM3 这种摘要算法很快影响可以忽略但 AES、RSA、SM2 这类算法尤其是 RSA 2048 和 SM2单次加密可能需要几毫秒甚至更久。在动辄几百并发的压测下这个开销会被放大。我遇到过最夸张的情况是一个登录接口用了 RSA 加密密码压测脚本里每次请求都临时生成 RSA 密钥对。这其实是把开销成倍放大而且完全没必要。RSA 加密只需要公钥应该把公钥固定下来只对密码做加密操作而不是每次重新生成密钥对。如果加密逻辑特别重还有两个优化思路一是把加密次数降下来比如 token 只在登录时加密一次后续接口直接用 token不再重复加密二是把加密计算从主线程挪开用 JMeter 的setUp Thread Group预先算好一批密文压测时从 CSV 里读取直接用但这种方式只适合明文可变性不强的场景实际落地要看接口设计。4.5 加密脚本报错时怎么快速定位脚本跑不起来最常见的错误有三类ClassNotFound第三方库没加载先检查 jar 是否放在lib/ext下。NoSuchAlgorithmException算法名写错比如把SM4/CBC/PKCS5Padding写成了SM4/CBC/PKCS7Padding或者没有注册 BC Provider。签名不一致脚本能跑但服务端报验签失败这大概率是拼接规则、编码或空值处理的问题不是脚本语法问题。排查工具我建议优先用 JMeter 自带的Debug Sampler。在请求后面加一个 Debug Sampler运行后查看结果树里的变量列表直接看sign、timestamp这些变量是否按预期生成。再配合log.info打印中间值基本能定位 90% 的问题。5. 从联调到压测加密参数脚本的工程化5.1 用 CSV 数据驱动避免硬编码参数单接口联调时参数写在 JSR223 脚本里无所谓。但到了压测阶段如果每个虚拟用户都用同一套参数不仅不符合真实场景很多系统还会因为重复数据产生干扰。最优做法是配合 CSV Data Set Config 做数据驱动。比如 CSV 里有三列name,amount,phoneJSR223 脚本里直接用vars.get(name)取出来参与加密加密完再放回变量。这样脚本本身不需要改只需替换 CSV 文件里的数据就能扩展压测量。用 CSV 时注意一个细节CSV 文件的编码建议保持 UTF-8如果有中文用 Excel 另存为 UTF-8 CSV 时容易带 BOM 头导致第一个参数值前面多个\uFEFF字符。签名时这一个小字符都可能让结果对不上。处理办法是用 Notepad 或 VSCode 把 CSV 转成 UTF-8 无 BOM 格式。5.2 把加密逻辑抽成公共 Jar多个脚本复用同一个系统通常有好几个 JMeter 脚本如果每个 JSR223 里都复制粘贴一大段加密代码后面一旦算法调整就要改几十处非常容易漏。我的做法是把加密逻辑写成一个 Java 工具类打成 jar 包放到 JMeter 的lib/ext下。JSR223 脚本里只需要一行调用import com.testtool.crypto.SignUtil String sign SignUtil.signHmacSha256(params, secretKey) vars.put(sign, sign)这样有一个额外的好处开发给的签名工具类本身就是 Java 实现的你几乎不用转换语法直接把核心方法抽出来做成工具类还能有效避免 Groovy 和 Java 在语法细节上的差异问题。维护成本低排查也容易。5.3 登录态、动态 token 与加密参数如何共存很多接口不是单独加密一个字段而是在登录接口拿到 token 后把 token 也参与后续接口的签名。这种场景要特别注意执行顺序先跑登录请求用 JSON Extractor 或 Regular Expression Extractor 提取 token。再把 token 存入变量供后续 JSR223 脚本取用。加密脚本里把 token 放进签名参数集合最后把加密后的值塞回请求。我曾经踩过一个坑登录返回的 token 里有特殊字符比如、/提取出来直接用了服务端验签始终失败。后来发现是提取到的 token 被当成了 URL 参数被解析成空格取值就变了。解决办法是在提取后做 URL 解码或者在放入签名前先统一处理。5.4 压测跑批时保留现场的小技巧压测时一旦签名失败最怕的是请求发出去后看不到当时生成的签名和请求体。正确做法是在 JSR223 脚本里加一个log.info把请求的 key 参数、签名结果、原始串都打印出来。在请求下加断言比如Response Assertion校验返回码或返回信息断言失败时把响应存到文件。用结果树或简单的后端监听器把失败样本单独导出。另外因为我是在压测机上跑JMeter 的日志文件默认会无限增长建议在jmeter.properties里配置日志滚动不然几个小时的压测下来jmeter.log 可能几个 G后续想排查问题都打不开。加密参数这块我在实际项目里吃过不少亏最深刻的体会是不要急于写脚本先花时间把加密规则、编码、密钥格式全部对齐宁可晚半天动手也别在错误的方向上写一堆代码。签名不一致的时候不要自己瞎猜直接让开发把服务端收到的参数打印出来两边一对比问题通常马上暴露。脚本跑通之后也别急着上大并发先单线程多跑几次确认每一次的 sign 都正确再逐步加压这样后面出问题时能少走很多弯路。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。