【精通JMeter接口测试-完结】05-JMeter通关收官!BeanShell和接口签名

前言

兄弟们,不知不觉这个【精通JMeter接口测试】系列已经走到了最后一篇。

从最初的环境搭建、断言、参数化,到后来的关联、控制器、我们一步步把 JMeter 从"能用"玩到了"精通"。

而今天这最后一块拼图,叫做 BeanShell + 接口签名

BeanShell 是 JMeter 中的"脚本瑞士军刀",能让你在测试过程中动态处理数据、读写文件、调用 Java 代码------几乎所有内置组件搞不定的逻辑,它都能兜底。

而接口签名(如 MD5、RSA、HmacSHA256)是现代 API 安全防篡改、防重放的核心手段。不会签名,很多企业级接口你连调都调不通。

本篇会把这两者彻底讲透:从 BeanShell 基础语法、常用内置变量,到如何用 BeanShell 前置处理器动态生成签名参数,再到封装成可复用的脚本。

学完这一篇,你手里的 JMeter 就真的"精通"了。

目录

一、Jmeter接口测试之BeanShell

1、什么是BeanShell

BeanShell是jmeter的扩展组件,因为jmeter它有好多东西是不支持的,比如某些加密算法函数,它不支持。此时我们只有通过BeanShell来扩展。

BeanShell他可以写Java代码和它自带的BeanShell代码。

BeanShell有很多种:

  1. 前置处处理器有BeanShell
  2. 定时器有BeanShell
  3. 采样器有BeanShell
  4. 后置处理器有BeanShell
  5. 断言有BeanShell
  6. 监听器有BeanShell

注意:

BeanShell他们都是一样的。只是你在什么时间去执行,举个例子:如果你在请求之前执行,那么你 就是前置处理器的BeanShell。

2、BeanShell常用的内置变量和语法规则

2.1 内置对象

  • vars:操作局部变量(单个线程组内使用)
  • props:操作全局变量(跨线程组/多线程使用)
  • prev:获取上一个请求的响应结果
  • log:打印日志(调试必备)

2.2 BeanShell 核心语法

没一句代码都要以分号结尾

2.2.1. 日志打印(调试用)
java 复制代码
// 功能:在JMeter日志中输出指定文字,用于调试代码
log.info("我可以用来调试。。。");



2.2.2. 局部变量操作(vars:单个线程内使用)
(1)vars.put():设置局部变量/操作局部变量

不能够跨线程组,只能在一个线程组里面使用的变量叫做局部变量。

java 复制代码
// 功能:设置局部变量,变量名age,变量值18(只能当前线程组使用)
vars.put("age","18");
(2)vars.get():获取局部变量

如图所示,比如我创建一个配置元件,用户定义的变量。我在里面加一个变量叫做李四。


获取用户定义的变量

java 复制代码
// 1. 获取用户自定义的变量,赋值给字符串变量n
String n = vars.get("name");
// 打印变量值
log.info(n);

获取正则表达式提取的变量

java 复制代码
// 2. 获取正则表达式提取器提取的变量(token)
String token = vars.get("csrf_token");
log.info(token);



获取其他BeanShell设置的变量

比如我有一个BeanShell后置处理程序

我在里面定义一个age,值为18

java 复制代码
// 3. 获取其他BeanShell设置的变量
String a = vars.get("age");
log.info(a);

2.2.3. 全局变量操作(props:跨线程/多线程使用)
(1)props.put():设置全局变量
java 复制代码
// 功能:设置全局变量,变量名www,变量值zhongge(所有线程组都能调用)
props.put("www","zhongge");

举个例子,我在线程组5中设置一个全局变量,怎么设置,我们在这个后置BeanShell中,添加一个name:"zhongge"

(2)props.get():获取全局变量

在线程组6里面添加一个前置BeanShell处理器。

java 复制代码
// 获取全局变量www的值
String w = props.get("www");
// 打印变量值
log.info(w);

2.2.4. 获取上一个请求的响应信息(prev)

那获取请求的响应信息这个是容易掉坑的。

首先,我们先来掉坑:

首先我们看登录接口如下图👇

于是我想获取他的响应状态码, Ok,咱老铁就把它写在前置处理器中,于是就右键接口,添加一个前置的BeanShell处理器

我们在里面写下如下代码:
获取上一个接口请求的响应状态码

java 复制代码
// 获取上一个接口请求的响应状态码(如:200、404、500)
log.info(prev.getResponseCode());

结果:报错了

请求是通过的呀,为什么会报错获取不到响应状态码呢?原因其实很简单。

你为什么要用前置处理器呢?前置处理器的话,它是在接口执行之前。你接口都没有执行,你哪儿来获取他的状态吗?是吧?你只有等接口执行完之后,他才会给你返回响应数据以及状态码等。

因此换句话说,我们这里不能用前置BeanShell,而是用后置BeanShell。

Ok,此时我们改为后置BeanShell

执行结果:使用后置处理器之后就正确了,👇


获取上一个接口请求的完整响应数据(转为字符串)

java 复制代码
// 获取上一个接口请求的完整响应数据(转为字符串)
log.info(prev.getResponseDataAsString());

这个也是一样的,注意一点就是我们得使用后置处理器。


3、BeanShell 调用外部代码

3.1. BeanShell 直接调用 Java 文件

Ok,我们先写个Java代码

然后把这个Java文件呢,放到我们的本地目录

当然这里不是说让老铁们去学习怎么写Java。我们到时候这些代码都是人家写好的,我们只需要会用就行。

我们先添加一个BeanShell取样器来测试一下:

测试代码如下:

java 复制代码
// 1. 引入本地Java文件(填写文件绝对路径)
source("E://ants//Test.java");

// 2. 创建Test类的对象,调用类中的add方法,传入参数3和5
int sum = new Test().add(3,5);

// 3. 将计算结果转为字符串并打印日志
log.info(sum.toString());

结果如下:

3.2. BeanShell 调用 打包好的 Jar 包

怎么去执行这个jar包呢?跟我一起做。

首先把jar包导入进来👇


代码:

java 复制代码
// 1. 有包就导入jar包中的test类(包名+类名)
//没有就算了

// 2. 创建对象调用方法,计算10+20
int sum = new Test().add(10,20);

// 3. 打印结果
log.info("相加的结果:" + sum.toString());

结果


3.3. BeanShell 调用 Python 代码(无参数)

1)首先,新建一个py文件,用于测试

py 复制代码
# E:/ants/test.py
def hello():
    return "我是Python脚本,调用成功啦!"

if __name__ == "__main__":
    # 给JMeter返回内容
    print(hello())
    print("200 调用正常")

2)其次就是 我们最后是需要执行你这个py代码的,所以此处我们得去找一下我们的python解析器

Ok,现在就加一个取样器,然后再执行下面的代码。

java 复制代码
// 导入Java流操作包,用于读取命令行执行结果
import java.io.BufferedReader;
import java.io.InputStreamReader;

// 拼接cmd命令:执行python脚本(cmd /c 执行完自动关闭窗口)
String command = "cmd /c D:/PythonProject/python.exe E:/ants/test.py";

// 获取Java运行时对象
Runtime rt = Runtime.getRuntime();
// 执行cmd命令,启动进程
Process pr = rt.exec(command);

// 等待Python脚本执行完成
pr.waitFor();

// 读取进程的输出流(获取Python脚本返回结果)
BufferedReader b = new BufferedReader(new InputStreamReader(pr.getInputStream()));
String line = "";
// 字符串拼接器,存储完整返回结果
StringBuilder response = new StringBuilder();

// 循环读取每一行输出
while ((line = b.readLine()) != null) {
    response.append(line);
}

// 转为字符串
String response_data = response.toString();
// 关闭流
b.close();

// 打印Python脚本执行结果
log.info(response_data);

注意:需提前安装 pip install argparse


3.4. BeanShell 调用 Python 代码(传1个参数)

py 复制代码
# 导入命令行参数工具,用来接收 -t 传过来的参数
import argparse

# 创建参数解析器对象
parser = argparse.ArgumentParser()

# 定义要接收的参数:-t 是参数名,必须传字符串,不能为空
parser.add_argument('-t', type=str, required=True, help='接收JMeter传过来的参数')

# 解析命令行传入的所有参数
args = parser.parse_args()

# 把传入的参数取出来,赋值给变量
param = args.t

# 打印内容,会被JMeter读取到日志里
print(f"JMeter传过来的参数是:{param}")
print("参数接收成功,脚本执行完毕")

执行代码:

java 复制代码
// 导入Java流读取工具,用来接收Python脚本返回的数据
import java.io.BufferedReader;
import java.io.InputStreamReader;

// 拼接执行命令:调用你电脑D盘的Python,执行E盘ants下的test_args.py,并且传参数 -t admin
String command = "cmd /c D:/PythonProject/python.exe E:/ants/test_args.py -t admin";

// 获取系统运行时对象,用来执行cmd命令
Runtime rt = Runtime.getRuntime();
// 执行上面拼接好的命令,启动Python脚本进程
Process pr = rt.exec(command);

// 暂停等待,等Python脚本全部执行完再往下走
pr.waitFor();

// 创建读取器,读取Python脚本打印出来的内容
BufferedReader b = new BufferedReader(new InputStreamReader(pr.getInputStream()));
// 定义临时变量,存每一行读取到的数据
String line = "";
// 字符串拼接工具,把Python所有返回内容拼到一起
StringBuilder response = new StringBuilder();

// 循环读取Python返回的每一行内容
while ((line = b.readLine()) != null) {
    response.append(line);
}

// 把拼接好的内容转成普通字符串
String response_data = response.toString();
// 关闭读取流,释放资源
b.close();

// 在JMeter日志中打印Python返回的结果
log.info("Python 带参运行返回:" + response_data);

注意:需提前安装 pip install argparse

结果:


3.5. BeanShell 调用 Python 代码(传2个参数)

创建py代码

py 复制代码
# 导入命令行参数解析库,用来接收JMeter传过来的参数
import argparse

# 创建参数解析器
parser = argparse.ArgumentParser()

# 接收第一个参数:-t 账号
parser.add_argument("-t", required=True, help="传入账号")

# 接收第二个参数:-p 密码
parser.add_argument("-p", required=True, help="传入密码")

# 解析所有传入的参数
args = parser.parse_args()

# 获取参数1:账号
username = args.t

# 获取参数2:密码
password = args.p

# 打印内容,返回给JMeter
print("接收参数成功!")
print(f"账号:{username}")
print(f"密码:{password}")
print("脚本执行完毕!")
java 复制代码
// 导入读取Python返回结果需要的工具包
import java.io.BufferedReader;
import java.io.InputStreamReader;

// 拼接CMD命令
// cmd /c = 执行命令后关闭窗口
// D:/PythonProject/python.exe = 你电脑的Python路径
// E:/ants/test_two_args.py = 你的Python脚本
// -t admin -p 123 = 传给Python的两个参数:账号admin、密码123
String command = "cmd /c D:/PythonProject/python.exe E:/ants/test_two_args.py -t admin -p 123";

// 获取系统运行对象,用来执行CMD命令
Runtime rt = Runtime.getRuntime();

// 执行命令,启动Python脚本
Process pr = rt.exec(command);

// 等待Python脚本完全运行结束,再继续执行后面代码
pr.waitFor();

// 创建读取流,读取Python打印出来的内容
BufferedReader b = new BufferedReader(new InputStreamReader(pr.getInputStream()));

// 定义变量,接收每一行内容
String line = "";

// 字符串拼接器,把所有返回结果拼在一起
StringBuilder response = new StringBuilder();

// 循环读取Python返回的每一行数据
while ((line = b.readLine()) != null) {
    response.append(line);
}

// 把最终结果转成字符串
String response_data = response.toString();

// 关闭流
b.close();

// 在JMeter日志打印Python返回的结果
log.info("Python双参数返回:" + response_data);

结果:


3.6. BeanShell 调用 Python 实现 RSA 加密

RSA规则:首先会生成公钥和私钥,然后用公钥来加密,用私钥来解密。

OK,咱先用Python代码写出这个加密的算法。这些算法的话复制粘贴就行了,然后我们再用Java去操作执行我们这个Python代码。这里不要懵,至于我们跑Python代码为什么要用Java去操作?我们3.7会解释。

Python RSA 加密代码:

py 复制代码
# 导入真正的RSA加密算法库
import rsa
# 导入传参工具
import argparse

# 解析传入的账号密码参数
parser = argparse.ArgumentParser()
parser.add_argument("-t", required=True)
parser.add_argument("-p", required=True)
args = parser.parse_args()

user = args.t
pwd = args.p

# ========== 这里才是真正的RSA加密算法开始 ==========
# 1. 生成公钥、私钥
pub_key, priv_key = rsa.newkeys(512)

# 2. 公钥加密账号、转16进制字符串方便JMeter接收
enc_user = rsa.encrypt(user.encode("utf-8"), pub_key).hex()
# 3. 公钥加密密码
enc_pwd = rsa.encrypt(pwd.encode("utf-8"), pub_key).hex()
# ========== RSA加密算法结束 ==========

# 用###分隔返回给JMeter
print(f"{enc_user}###{enc_pwd}")

接下来就用我们的BeanShell去执行:

java 复制代码
// 导入用于读取Python输出内容的工具类
import java.io.BufferedReader;
// 导入将字节流转换为字符流的工具类
import java.io.InputStreamReader;

// 1. 拼接CMD命令:调用Python程序执行加密脚本
// cmd /c                     执行完命令后自动关闭CMD窗口
// D:/PythonProject/python.exe   你电脑上的Python解释器路径
// E:/ants/rsa_encrypt.py       你的RSA加密Python脚本路径
// -t admin                    向脚本传递参数:用户名 admin
// -p 123                      向脚本传递参数:密码 123
String command = "cmd /c D:/PythonProject/python.exe E:/ants/rsa_encrypt.py -t admin -p 123";

// 2. 获取Java运行时环境对象,用于执行操作系统命令
Runtime rt = Runtime.getRuntime();
// 3. 执行拼接好的CMD命令,启动Python进程
Process pr = rt.exec(command);
// 4. 等待Python脚本执行完毕,再执行后面的代码(防止还没跑完就读取结果)
pr.waitFor();

// 5. 获取Python进程的输出流,并包装成缓冲读取器(方便读取打印内容)
BufferedReader b = new BufferedReader(new InputStreamReader(pr.getInputStream()));
// 定义临时字符串变量,存储每一行读取到的内容
String line = "";
// 定义字符串构建器,拼接Python返回的所有加密结果
StringBuilder response = new StringBuilder();

// 6. 循环读取Python输出的每一行内容,直到读完为止
while ((line = b.readLine()) != null) {
    // 将读到的内容追加到结果字符串中
    response.append(line);
}

// 7. 将拼接完成的最终结果转为字符串
String response_data = response.toString();
// 8. 关闭读取流,释放系统资源
b.close();

// 9. 按照分隔符 ### 拆分字符串:[加密账号, 加密密码]
String[] msg = response_data.split("###");
// 打印输出加密后的账号
log.info("账号:" + msg[0]);
// 打印输出加密后的密码
log.info("密码:" + msg[1]);

// 10. 将加密结果存入JMeter变量,方便接口调用
// 后续在请求参数中直接使用 ${usn} 和 ${psw}
vars.put("usn", msg[0]);
vars.put("psw", msg[1]);

注意:需安装 pip install rsa如果你的结果没运行出来,那就是你这个东西没安装。


3.6.1 实战演示

接口文档

简要描述:RSA加密接口

请求URL:http://116.62.63.211:5000/rsalogin

请求方式:POST

请求参数:

参数名 必选 类型 说明 备注
username string 用户名:admin 用户名必须RSA加密处理
password string 密码:123 密码必须RSA加密处理

RSA加密公钥

复制代码
-------BEGIN RSA PUBLIC KEY-------
MIGJAoGBALO7UPE26anTGHND2Q54zYYPusDx+tbO1Yia7zoxpZediw+Baea7aFZC
J+ZvWd5ZBTopuWvb8hNkY24eBHcXN0pU32WjsH9REp1kXhxbndnw+u3diaoUFqVc
66xl+LXEo1Y9oDWfkGCir2JnN0aieUiPlHDLhmc+LII/ZDspITKDAgMBAAE=
-------END RSA PUBLIC KEY-------

RSA加密私钥

复制代码
-------BEGIN RSA PRIVATE KEY-------
MIICYAIBAAKBgQCzu1DxNump0xhzQ9kOeM2GD7rA8frWztWImu86MaWXnYsPgWnm
u2hWQifmb1neWQU6Kblr2/ITZGNuHgR3FzdKVN9lo7B/URKdZF4cW53Z8Prt3Ymq
FBalXOusZfi1xKNWPaA1n5Bgoq9iZzdGonlIj5Rwy4ZnPiyCP2Q7KSEygwIDAQAB
AoGAP8Rxz2NAO/SddCfaVvwdTzc9dz9jU9tGf5UY03jR250VvYaY4DgVqKUIKwXO
yMikxiIPm2kJ1j+D14nmmfYdn0FGltuwbubNqFMgZC33dx1fyPFtC2ChBRg2VpY9
K/WM0jvPpA2j9SUtEpLSK2dPnpUYOerF0fWljTA6ydzun0ECRQDPu9bUUYylVfCH
AHZ+5TqtiI5O0xqYs2ZkgkkvtBYxd7a7A3zHclKbiMHs6x7WmfReTaIV/P896msQ
w4Newr/KYYH5qQI9AN195O2DYcGhIqS8hPv2uy5cXQoJ4chp7NnkABL2dJ0BkTD7
K1G/uafdb/rCqTtp5TSQcM0hqWLuYfleSwJEQLnCYj1WmV2BqE7YnQlHkzJtRNo5
+0JKsR53N0nhcGBgqzyW8H4KhzPBWNSZszwdDBUcX8WIsf4MhhilIi2EHMbs/KEC
PCAPLumqEKZEOo15tA38Yo+NUvI3B/VIT74iIFieT7bCGU/rqxOEGs1PYfv73R76
bvCyz4EBlFD7jDbaDQJFAJjl9poUrOHwh0ybNC13lCFhH4oWILufUoaGfa25jFuE
MYS7Ri5vwf8tcbbdZQbILa2LVhOxgmqCCUoav3pPe8YdEYEP
-------END RSA PRIVATE KEY-------

返回示例

bash 复制代码
{"error_code":0,"message":"RSA加密登陆成功!"}

返回参数说明:

参数名 类型 说明
message string success表示登陆成功

1)好的,此时python代码中的秘钥得修改一下:

py 复制代码
# 1. 导入命令行参数接收库(用来接收JMeter传的账号、密码)
import argparse

# 2. 导入Base64编码库(把加密后的二进制转成字符串,接口才能识别)
import base64

# 3. 导入RSA加密库(核心加密工具)
import rsa

# 定义RSA加密函数(给JMeter调用)
def rsa_jiami():
    # ===================== 【第一步:接收JMeter传过来的参数】 =====================
    # 创建参数解析器
    parse = argparse.ArgumentParser()

    # 定义接收参数:-t 代表用户名(必须传)
    parse.add_argument("-t", required=True)
    
    # 定义接收参数:-p 代表密码(必须传)
    parse.add_argument("-p", required=True)

    # 解析传进来的参数
    args = parse.parse_args()

    # 从参数中取出 用户名(来自 -t)
    username = args.t
    
    # 从参数中取出 密码(来自 -p)
    password = args.p

    # ===================== 【第二步:加载接口给的固定公钥】 =====================
    # 接口文档提供的固定RSA公钥(前后端约定好,不能改)
    public_key_str = """-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBALO7UPE26anTGHND2Q54zYYPusDx+tbO1Yia7zoxpZediw+Baea7aFZC
J+ZvWd5ZBTopuWvb8hNkY24eBHcXN0pU32WjsH9REp1kXhxbndnw+u3diaoUFqVc
66xl+LXEo1Y9oDWfkGCir2JnN0aieUiPlHDLhmc+LII/ZDspITKDAgMBAAE=
-----END RSA PUBLIC KEY-----"""
    
    # 把字符串公钥 变成 Python 能识别的公钥对象
    pubkey = rsa.PublicKey.load_pkcs1(public_key_str.encode())

    # ===================== 【第三步:固定加密!不再随机!】 =====================
    # 核心修复:接口需要 "无填充 / 固定结果"
    user_enc = rsa.encrypt(username.encode('utf-8'), pubkey)
    pwd_enc = rsa.encrypt(password.encode('utf-8'), pubkey)

    # 把加密后的二进制 转成 Base64字符串(接口要求的格式)
    user_base64 = base64.b64encode(user_enc).decode("utf-8")
    pwd_base64 = base64.b64encode(pwd_enc).decode("utf-8")

    # ===================== 【第四步:输出给JMeter】 =====================
    # 输出格式:加密账号###加密密码,方便JMeter切割使用
    print(f"{user_base64}###{pwd_base64}")

# 程序入口:运行加密函数
if __name__ == '__main__':
    rsa_jiami()

2)我们就加这个接口如下,👇


3)右键接口添加一个前置处理器,用于得出加密之后的账号,必须是前置处理器,因为我们要加密之后再请求


4)OK,那么此时我们就使用BeanShell脚本对admin和123进行加密,结果如下:


5)将结果加密后的账号复制进去

注意,因为我们存值了,所以直接使用${}取值

注意事项

6)结果:正确


3.7. 解释为什么我们在这个里面它不直接执行Python代码,而是用Java去执行Python代码呢?

你想过没有?我们上述是要去跑Python代码的,但是这个Python它的执行过程是用Java来操作的, 这是为什么呢?

原因如下:

核心一句话:

因为 JMeter 本身只支持 Java,不支持直接跑 Python!

  1. 先搞清楚:JMeter 是什么语言写的?
    JMeter 是纯 Java 开发的工具
    它的内核、运行引擎、BeanShell 全都跑在 Java 环境里。
    它根本不认识 Python 代码!
    你直接在 JMeter 里写 Python,它完全识别不了,不知道是什么语法。
  2. 那我们明明用 JMeter,为什么还要用 Python?
    像 RSA 加密、AES 加密、接口签名、复杂算法、调用第三方接口这些功能;
    Python 写起来超级简单、代码少、上手快。
    换成纯 Java 写,代码又多又繁琐,写起来很费劲。
  3. 所以才有了这种搭配逻辑:
    JMeter(Java) → 调用 Python → 拿到处理好的结果
    打个比方:
    你不会说英语,你朋友会英语;
    你让朋友帮你跟老外沟通,最后把结果转告你。
    JMeter 相当于你,Python 相当于会英语的朋友,BeanShell 就是传话调用的中间人。
  4. 为什么 JMeter 不能直接运行 Python?
    JMeter 没有内置 Python 解释器和运行环境;
    JMeter 只识别 Java 的 class、jar、BeanShell 语法;
    Python 是独立脚本语言,不能直接嵌入到 JMeter 内核里运行。
    只能靠 Java(BeanShell)调用系统 CMD 命令,拉起 Python 脚本执行,再拿回结果。
  5. 执行流程逻辑
    JMeter 界面
    往下走到 BeanShell(Java 代码)
    再调用 cmd / python.exe 程序
    运行 Python 脚本做加密、算法处理
    把处理完的结果回传给 JMeter
    JMeter 拿到结果直接用于接口请求
  6. 最通俗总结
    JMeter 只会 Java 语法,不认 Python;
    但 Python 写加密、算法更简单省事;
    所以只能用 Java 当中间人,调用 Python 帮忙干活。
  7. 记住这三点就够用
    JMeter 底层是 Java 开发的;
    JMeter 原生不能直接运行 Python 脚本;
    只能通过 BeanShell 写 Java 代码,间接调用 Python 做加密、签名、复杂算法。
    一句话终极总结:

不是不想直接用 Python,是 JMeter 原生根本不支持,只能用 Java 中转间接调用。


二、Jmeter接口测试之接口签名

1、为啥要有接口签名?

现在前后端、APP 和服务器都是接口互相传数据。

如果不加签名,坏人就能搞三件坏事:

  1. 偷偷看到你的接口账号、密钥,直接盗用;
  2. 中途篡改你传的参数,比如把商品价格改成1块、把别人订单改成自己的;
  3. 拿着你之前发过的接口请求,重复疯狂提交,恶意刷单、重复下单。

所以公司就搞了一套自定义加密规则 ,不是普通 RSA/MD5 那种单纯加密,

接口签名 Sign,专门防泄露、防篡改、防重复提交。

2、接口签名到底是什么?

接口签名:Sign就是一个特殊的自定义加密。

Sign 就是公司自己定的一套专属加密规则

不是固定算法,每家公司规则都稍微不一样,但套路大同小异。

把接口所有参数按规矩拼接、加盐、加随机号、加时间,最后 MD5 加密转大写,生成一串唯一验证码 ,这个验证码就是 Sign。

接口签名实际针对的是接口参数
后端给你定了一套「签名规则」,你必须按规则算出一个 sign,再把它放到请求头里一起发过去。 后端收到请求,会按一模一样的规则,自己再算一遍 sign。 两个 sign 一模一样:放行,接口请求成功 不一样:直接判定你参数被篡改、请求非法,接口直接报错,根本不会处理你的请求

3、接口签名三大核心作用

  1. 防止接口密钥泄露
    普通人看不懂拼接规则和加密串,坏人拿不到真实账号密钥。
  2. 防止接口参数被篡改
    参数必须按规则排序拼接,改任何一个字,生成的 Sign 立马变,后端一比对就知道被改了,直接拒绝请求。
  3. 防止接口被重复提交
    随机流水号 nonce + 时间戳 timestamp
    限定十几秒内有效,过期作废,坏人拿旧请求重复发也没用。

4、一步步拆解签名规则

4.1. 收集所有接口参数(查询字符串或Body参数等等),按键名 ASCII 升序排序

比如接口传了 c=3,a=2,b=1

乱序不行,必须按字母顺序排成:a、b、c

固定顺序,不然后端和前端拼的字符串不一样,签名对不上。

4.2. 参数键值用=连,多个参数用&连

排好序后拼成:a=2&b=1&c=3

4.3. 头部拼平台给的 appid、appsecret

appid、appsecret 相当于项目的账号密码 ,只有正规合作方才有。

拼完:appid=admin&appsecret=123&a=2&b=1&c=3

4.4. 尾部拼随机流水号 nonce

一串随机十位以上字符串,作用:防重复提交

拼完:appid=admin&appsecret=123&a=2&b=1&c=3&nonce=1234567890

4.5. 再尾部拼时间戳 timestamp

当前时间戳,一般限定10分钟内有效 ,超时就失效,防止旧请求被拿去复用。

拼完一长串完整原始字符串。

4.6. 整串做 MD5 加密,再转大写

加密完生成一长串字符,就是 Sign 签名值

4.7. 把 Sign 放到请求头里一起发给后端

后端拿同样规则再拼一遍、加密一次,对比你传过来的 Sign:

一样 = 合法请求,正常通过;

不一样 = 被篡改、过期、非法请求,直接拦截。

5、实战

如图所示,咱们要请求这个接口,那么这个接口里面有很多参数,我们按照上述步骤一步一步做。

5.1、先把你这个接口的所有参数全扒出来

1). URL 查询参数(路径里的)

http://47.107.116.139/phpwind/index.php?m=u&c=login&a=dorun

这里的参数是:

  • m=u
  • c=login
  • a=dorun
2). Body 请求体参数(表单里的)
  • username=lisi
  • password=123456
  • csrf_token=${token}(这个是动态获取的,先不算进排序)
  • backurl=http://47.107.116.139/phpwind/
  • invite=(空值)

5.2、这个接口的「通用签名规则」是什么?

接口签名 Sign,就是后端定的一套固定拼接+加密流程 ,防止参数被篡改、请求被盗用/重放。

你这个接口的签名规则,就是按下面7步走:

  1. 收集所有非动态参数:包括URL查询参数 + Body表单参数(csrf_token这种动态值除外)
  2. 把所有参数的 key,按ASCII码升序排列
  3. 把每个参数拼成 key=value 格式,多个参数用 & 连接
  4. 头部拼接平台给的 appidappsecret(项目的"账号密码")
  5. 尾部拼接 nonce(随机流水号,防重复提交)
  6. 尾部拼接 timestamp(时间戳,限定10分钟内有效)
  7. 对整串字符串做 MD5加密 ,再转大写,得到 sign,放到请求头里

5.3、要知道为什么要按这个规则搞?

  1. 按key排序:前后端拼接顺序必须一致,不然签名永远对不上
  2. 固定拼接格式:key=value&,统一格式,避免歧义
  3. 加appid/appsecret:证明你是合法调用方,不是随便谁都能发请求
  4. 加nonce:防止别人拿你发过的请求,重复提交刷单
  5. 加timestamp:防止旧请求被别人拿去复用,过了时间就失效
  6. MD5加密转大写:把明文变成不可逆的密文,别人看不懂也改不了参数

5.4、我们一起走一遍完整流程

第1步:收集所有非动态参数(URL+Body)

先把所有要参与签名的参数列出来:

bash 复制代码
URL参数:m=u, c=login, a=dorun
Body参数:username=lisi, password=123456, backurl=http://47.107.116.139/phpwind/, invite=

(csrf_token是动态的,规则里说"动态参数除外",所以不参与排序和拼接)

第2步:按key的ASCII码升序排序

按字母顺序 a → b → c → i → m → p → u 排序:

bash 复制代码
a=dorun
backurl=http://47.107.116.139/phpwind/
c=login
invite=
m=u
password=123456
username=lisi
第3步:拼成 key=value& 格式
bash 复制代码
a=dorun&backurl=http://47.107.116.139/phpwind/&c=login&invite=&m=u&password=123456&username=lisi
第4步:头部拼接 appid 和 appsecret

假设项目密钥是:

  • appid=admin
  • appsecret=123
    拼接后:
bash 复制代码
appid=admin&appsecret=123&a=dorun&backurl=http://47.107.116.139/phpwind/&c=login&invite=&m=u&password=123456&username=lisi
第5步:尾部拼接 nonce(随机流水号)

假设随机号是 nonce=1234567890

bash 复制代码
appid=admin&appsecret=123&a=dorun&backurl=http://47.107.116.139/phpwind/&c=login&invite=&m=u&password=123456&username=lisi&nonce=1234567890
第6步:尾部拼接 timestamp(时间戳)

假设当前时间戳是 timestamp=1746000000

bash 复制代码
appid=admin&appsecret=123&a=dorun&backurl=http://47.107.116.139/phpwind/&c=login&invite=&m=u&password=123456&username=lisi&nonce=1234567890&timestamp=1746000000

此处我们时间戳怎么生成的?

补充BeanShell 获取时间戳(Java原生代码)
java 复制代码
// 导入Java时间包
import java.time.Instant;

// 获取当前时间戳(毫秒级)
long timestamp = Instant.now().toEpochMilli();
// 打印时间戳
log.info("时间戳:"+timestamp);
第7步:对整串做 MD5 加密,再转大写

MD5加密 在我们的jmeter中 MD5函数如下所示:注意jmeter5.6.3是digest函数

加密后得到:

bash 复制代码
2b04d7d88195dbf9af934ffbcb800493

再将他变为大写:

bash 复制代码
2B04D7D88195DBF9AF934FFBCB800493

这串大写字母数字,就是你最终的接口签名 sign

第8步:把sign放到请求头里带给我们的后端

我们请求头中是键值对的方式。

比如我们请求头里加一行:

java 复制代码
Authorization: Sgin 2B04D7D88195DBF9AF934FFBCB800493

为什么是Authorization: Sgin 2B04D7D88195DBF9AF934FFBCB800493 不是Authorization Sgin : 2B04D7D88195DBF9AF934FFBCB800493

一个例子看懂:

那么后续我们后端会按一模一样的规则,再拼一遍字符串、算一次MD5,和你传的sign对比,一致就放行,不一致就拦截。


接下来我们用这个接口的数据,写一个完整的 JMeter BeanShell 脚本,自动完成参数排序、拼接、生成签名👇

5.5 接口签名BeanShell脚本(Java语言版本)

注意:这里我们必须写在前置处理器中(后面会有解释)

java 复制代码
// ====================== 【1】导入需要的包 ======================
import org.apache.commons.codec.digest.DigestUtils;
import java.util.Map;
import java.util.TreeMap;
import java.util.Iterator;

// ====================== 【2】收集所有参数(修复版!) ======================
Map params = new TreeMap(); // 去掉 <>,BeanShell 兼容

// 放入所有参数(自动ASCII排序)
params.put("a", "dorun");
params.put("c", "login");
params.put("m", "u");
params.put("username", "lisi");
params.put("password", "123456");
params.put("backurl", "http://47.107.116.139/phpwind/");
params.put("invite", "");

// ====================== 【3】拼接 key=value&key=value ======================
StringBuilder sb = new StringBuilder();
Iterator it = params.entrySet().iterator();

while (it.hasNext()) {
    Map.Entry entry = (Map.Entry) it.next();
    String key = (String) entry.getKey();
    String value = (String) entry.getValue();
    sb.append(key).append("=").append(value).append("&");
}

String paramStr = sb.substring(0, sb.length() - 1);

// ====================== 【4】拼接 appid + appsecret ======================
String appid = "admin";
String appsecret = "123";
String signStr = "appid=" + appid + "&appsecret=" + appsecret + "&" + paramStr;

// ====================== 【5】拼接 nonce ======================
String nonce = "1234567890";
signStr = signStr + "&nonce=" + nonce;

// ====================== 【6】拼接 时间戳 ======================
long timestamp = System.currentTimeMillis() / 1000;
signStr = signStr + "&timestamp=" + timestamp;

// ====================== 【7】MD5 + 大写 ======================
String sign = DigestUtils.md5Hex(signStr).toUpperCase();

// ====================== 【8】存入变量 ======================
vars.put("sign", sign);
vars.put("timestamp", String.valueOf(timestamp));
vars.put("nonce", nonce);

// ====================== 打印日志 ======================
log.info("最终签名字符串:" + signStr);
log.info("生成签名:" + sign);

测试结果:

5.6 最后一步:请求头配置(必须做)

添加 HTTP信息头管理器

名称
Authorization Sign ${sign}

🚀 Ok,到这我们就演示结束了。那需要补充一点就是,由于我没有找到对应的接口,我们上述的演示接口只是带大家去看一下这个接口签名要走的流程,最终去运行的话,因为人家的,呃,需求里面就不需要这个东西,所以我就运行不了,后续我找到对应的例子,会给老铁们更新。

但是流程都是上述的例子,我们这里只是差了一步运行而已😁


5.7 疑问1🤔我们有接口签名,那是不是就不用写参数了?

答: 先给结论,

不是 Body、查询参数不用写了

而是:Body 和 查询字符串 参数 照样要原样写、照样要带

额外再多带一个:请求头里放 Sign 签名


接口签名逻辑是:

  1. 你发请求时
  • URL 查询参数 照样要写 m=u&c=login&a=dorun
  • Body 表单参数 照样要填 username、password、backurl、invite

这些参数一个都不能省,该传还得传

  1. 只是多做了一件事:

    所有这些 URL+Body 参数

    按规则排序 → 拼接 → 加appid/随机号/时间戳 → MD5加密

    生成一串 Sign 签名

  2. 最后额外在请求头里带上:

    Authorization: Sign 生成的那串MD5大写签名


打个最简单比方

你去车站坐车:

  1. 你本身人要去、身份证要带(相当于:URL参数、Body参数照样传)
  2. 还要额外多带一张车票(相当于:请求头里带 Sign 签名)

不能说:有车票了,人跟身份证就不用了 ❌

必须:人+身份证照常带,再额外多带车票 ✅


5.8 疑问2🤔那接口签名BeanShell脚本我们是写在哪里呢?是写在后置处理器还是预处理器?

答: 直接给你标准答案

写在 HTTP请求 的 前置处理器

接口签名脚本 → 必须放在前置处理器(BeanShell 预处理器)


为什么放前置处理器?不放后置处理器?

前置处理器:发接口请求 之前 先执行

后置处理器:发完接口请求、拿到响应后 才执行

接口签名的逻辑:

  1. 先发请求之前,必须先把参数排序、拼接、算出 Sign
  2. 把 Sign 生成好,存成 JMeter 变量 ${sign}
  3. 然后再发 HTTP 请求,请求头引用 ${sign}

顺序必须是:
预处理器执行(算签名) → 再发接口请求

后置处理器是请求都发完了才跑,那时候签名都晚了,根本没用。


结构图

复制代码
HTTP请求
 ├─ 前置处理器(BeanShell预处理器)  ← 写签名脚本,在这里生成sign
 └─ HTTP信息头管理器                 ← 引用 ${sign} 带到请求头

5.9 总结

一句话,上述折腾下来:

接口签名,本质就是只针对「接口业务参数」做加密校验


三、写在最后

🎯 看到这里,辛苦啦!歇一歇,喝口水,让眼睛放松一下
这篇笔记我写了很久,如果对你有哪怕一点点帮助,请点一下「关注」
后面我还会继续分享很多自学干货笔记,都是自己边学边总结的,依然会是零基础能看懂、每一步都截图的那种详细笔记
我是学生,没什么能送你的,只能保证每一篇都认真写、不藏私。
我能给的,就是一篇一篇亲手整理的、零基础也能看懂的干货笔记。
你的每一次关注,都是我继续写下去的动力。
谢谢你的时间,我们下一篇笔记见 👇
(如果这篇笔记有写错的地方,也请一定在评论区告诉我,我会第一时间修改)

兄弟们,到这里咱们精通JMeter接口测试完结撒花了 🌸🌸🌸

最后,要感谢每个点赞、评论、催更的兄弟 ------ 没有你们的正反馈,这个系列很难坚持到完结 ❤️

下一系列见咯,兄弟们~~

咱们下一系列将 拿下 Postman


相关推荐
某人辛木1 天前
JMeter下载安装配置
jmeter
查拉图斯特拉面条2 天前
JMeter脚本中断排查:CSV配置导致线程提前终止
jmeter
lifewange5 天前
JMeter InfluxDB 后端监听器 全参数详解
jmeter
川石课堂软件测试6 天前
技术分享|JMeter接口与性能测试实战
数据库·功能测试·测试工具·jmeter·单元测试·postman·prometheus
弹简特6 天前
【精通JMeter接口测试】03-JMeter 接口测试持续集成踩坑记:jtl 转 Allure 报告、Jenkins 定时执行、CSP 样式劫持全解决
jmeter·自动化·jenkins
晨+燕6 天前
JMeter中如何定位到某个具体的类来自于哪个jar包
python·jmeter·jar
_周游8 天前
【软件测试】使用JMeter进行压力测试_2
jmeter·压力测试
你这个想法好9 天前
深度解析 JMeter 性能测试:从插件安装到,“阶梯线程组”下,“仅一次控制器”失效的解决方案
jmeter