这周最烦的一件事,是压测环境里所有请求突然开始报 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":"U2FsdGVkX1+3cNf..."} | 敏感字段密文,结构仍可读 | 对指定字段做加密后传参 |
| 整体加密 | {"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,而不是 BeanShell
2.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、SM4,JDK 默认是不支持的,必须引入 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> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk18on</artifactId> <version>1.78</version> </dependency>2.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 + "×tamp=" + 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 都是这种套路。典型规则是:把业务参数按参数名升序排列,排除空值和签名本身,拼成key1=value1&key2=value2格式,最后拼接密钥,再做 MD5、SHA-256 或 HmacSHA256。
直接给一个可以在 JSR223 里跑的完整模板,假设业务参数有name、amount,密钥是abc123:
import 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 升序 TreeMap<String, 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() + "key=abc123" // 计算 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 = "appId=test×tamp=1710000000&key=密钥" 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/PKCS5Padding:
Security.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 文件里读出来时带了一个换行或空格,签名怎么算都不对。
还有一个隐藏坑是数字类型的一致性。接口文档说amount=100,但那是一个整数,开发在签名时可能用的是"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,phone,JSR223 脚本里直接用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 都正确,再逐步加压,这样后面出问题时能少走很多弯路。