1. 为什么需要处理加密参数?
在性能测试和接口测试中,加密参数的处理一直是让测试工程师头疼的问题。我见过太多团队因为加密参数处理不当,导致测试工作无法开展或者测试结果失真的情况。现代Web应用和API服务中,出于安全考虑,几乎所有关键业务接口都会对请求参数进行加密处理。
常见的加密场景包括:
- 登录接口中的密码字段(通常采用AES、RSA或MD5加密)
- 支付接口中的金额和订单信息
- 敏感数据传输时的整体报文加密
- 防篡改机制中的签名参数
以最常见的登录场景为例,前端通常会先对密码进行加密再传输。如果我们直接用JMeter发送明文的密码参数,服务器会直接拒绝这个请求。这就是为什么我们需要在JMeter中模拟真实的加密过程。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. JMeter处理加密参数的四种核心方案
2.1 使用JSR223预处理器+Groovy脚本
这是我在实际项目中最常用的方案,灵活性强且性能较好。具体操作步骤如下:
- 在HTTP请求下添加一个JSR223 PreProcessor
- 语言选择Groovy(性能最佳)
- 编写加密逻辑脚本
groovy复制import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec
import org.apache.commons.codec.binary.Base64
// AES加密示例
String key = "1234567812345678"
String originalText = vars.get("password") // 从变量获取原始密码
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes("UTF-8"), "AES")
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
byte[] encryptedBytes = cipher.doFinal(originalText.getBytes("UTF-8"))
String encryptedText = Base64.getEncoder().encodeToString(encryptedBytes)
vars.put("encryptedPassword", encryptedText) // 存储加密结果到变量
重要提示:在实际项目中,密钥管理要特别注意安全,不要硬编码在脚本中。可以通过JMeter属性或外部文件来管理密钥。
2.2 使用BeanShell调用Java代码
如果项目中有现成的Java加密工具类,可以通过BeanShell直接调用:
java复制import com.yourcompany.util.EncryptUtil;
String original = vars.get("password");
String encrypted = EncryptUtil.aesEncrypt(original);
vars.put("encryptedPassword", encrypted);
这种方法的好处是可以复用已有的Java代码,但性能比Groovy稍差。
2.3 使用__groovy函数直接加密
对于简单的加密需求,可以直接在参数值中使用__groovy函数:
code复制${__groovy(org.apache.commons.codec.digest.DigestUtils.md5Hex(vars.get("password")),)}
这种方法适合单行能解决的加密场景,比如MD5加密。
2.4 使用自定义JMeter插件
对于复杂的加密需求或高频使用的加密算法,可以考虑开发自定义JMeter插件。这种方式需要Java开发能力,但可以提供最好的性能和用户体验。
3. 实战案例:测试加密登录接口
让我们通过一个完整的例子来演示如何处理加密参数。假设我们有一个登录接口,要求:
- 用户名明文传输
- 密码需要AES加密
- 需要添加时间戳参数
- 需要生成请求签名
3.1 测试计划结构搭建
- 线程组
└─ HTTP请求(登录接口)
├─ JSR223 PreProcessor (密码加密)
├─ JSR223 PreProcessor (生成时间戳)
└─ JSR223 PreProcessor (生成签名)
3.2 密码加密实现
groovy复制// AES加密配置
String key = props.get("aes.key") // 从JMeter属性获取密钥
String password = vars.get("rawPassword")
// 执行AES加密
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes("UTF-8"), "AES")
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
String encryptedPassword = Base64.getEncoder().encodeToString(
cipher.doFinal(password.getBytes("UTF-8"))
)
vars.put("encryptedPassword", encryptedPassword)
log.info("加密后的密码:" + encryptedPassword)
3.3 时间戳生成
groovy复制vars.put("timestamp", String.valueOf(System.currentTimeMillis()))
3.4 请求签名生成
groovy复制String username = vars.get("username")
String encryptedPassword = vars.get("encryptedPassword")
String timestamp = vars.get("timestamp")
String secret = props.get("api.secret")
// 按规则拼接签名字符串
String signStr = username + encryptedPassword + timestamp + secret
// 计算MD5签名
String sign = org.apache.commons.codec.digest.DigestUtils.md5Hex(signStr)
vars.put("sign", sign)
3.5 HTTP请求参数配置
在HTTP请求中,这样设置参数:
- username: $
- password: $
- timestamp: $
- sign: $
4. 性能优化与常见问题
4.1 加密性能优化技巧
- 缓存加密实例:对于需要频繁创建加密对象的情况,可以使用JMeter的props对象缓存:
groovy复制Cipher cipher = props.get("aesCipher")
if(cipher == null) {
cipher = Cipher.getInstance("AES/ECB/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
props.put("aesCipher", cipher)
}
-
使用更高效的算法:MD5比SHA1更快,AES的ECB模式比CBC模式更快(但安全性稍低)
-
减少不必要的加密:对于相同的输入值,可以缓存加密结果
4.2 常见问题排查
问题1:加密结果与前端不一致
- 检查加密算法和模式是否一致(如AES/CBC/PKCS5Padding)
- 检查密钥是否相同
- 检查字符编码(通常使用UTF-8)
- 检查是否有多余的base64编码/解码
问题2:性能测试时加密成为瓶颈
- 使用Groovy而不是BeanShell
- 预加载加密类(在测试计划中添加一个仅执行一次的Setup线程组)
- 考虑使用更简单的加密算法或mock加密结果
问题3:动态参数加密问题
- 确保在加密前变量已经正确赋值
- 使用vars.get()和vars.put()时注意变量作用域
- 对于复杂的动态参数,可以先打印出来检查
5. 高级应用场景
5.1 处理非对称加密(RSA)
对于RSA加密,通常需要先获取服务器的公钥。这里给出一个完整示例:
groovy复制// 从文件读取公钥
String publicKeyStr = new File("public_key.pem").text
publicKeyStr = publicKeyStr.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s", "")
byte[] keyBytes = Base64.getDecoder().decode(publicKeyStr)
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes)
KeyFactory keyFactory = KeyFactory.getInstance("RSA")
PublicKey publicKey = keyFactory.generatePublic(spec)
// 执行加密
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding")
cipher.init(Cipher.ENCRYPT_MODE, publicKey)
byte[] encrypted = cipher.doFinal(vars.get("password").getBytes("UTF-8"))
vars.put("encryptedPassword", Base64.getEncoder().encodeToString(encrypted))
5.2 处理动态盐值加密
有些系统会返回动态salt用于加密,处理流程如下:
- 先调用获取salt的接口
- 从响应中提取salt值
- 使用salt加密密码
- 发送登录请求
groovy复制// 假设已经获取到salt并存入变量salt
String password = vars.get("password")
String salt = vars.get("salt")
String saltedPassword = password + salt
String encrypted = org.apache.commons.codec.digest.DigestUtils.sha256Hex(saltedPassword)
vars.put("encryptedPassword", encrypted)
5.3 处理全报文加密
对于要求整个请求体加密的场景:
- 先构建原始JSON
- 对整个JSON进行加密
- 发送加密后的数据
groovy复制import groovy.json.JsonBuilder
def requestBody = [
username: vars.get("username"),
password: vars.get("password"),
timestamp: System.currentTimeMillis()
]
String json = new JsonBuilder(requestBody).toPrettyString()
String encrypted = encrypt(json) // 调用加密方法
// 设置请求体
sampler.addNonEncodedArgument("", encrypted, "")
sampler.setPostBodyRaw(true)
6. 安全最佳实践
-
密钥管理:
- 不要将密钥硬编码在脚本中
- 使用JMeter属性文件(-J参数传入)
- 或者使用密钥管理系统
-
敏感数据处理:
- 在日志中不要打印完整加密数据
- 使用log.debug()替代log.info()
- 测试完成后清理包含敏感数据的文件
-
加密算法选择:
- 生产环境测试使用与实际系统相同的算法
- 压测环境可以考虑使用更简单的算法提高性能
-
脚本版本控制:
- 将加密逻辑提取到单独的脚本文件
- 使用版本控制系统管理
- 方便算法变更时的统一更新
