最近工作中使用jmeter进行压力测试,压测的一系列接口是提供给外部调用的数据接入接口。这类接口是用来给业务系统传递数据的。出于安全的考虑,对入参进行了签名,将签名参数和入参一并传入,由业务系统对签名参数进行验证。这种情况和普通接口需要传入token不一样,token可以由登陆接口获取,而签名只能使用函数方法得到。
对于这种业务场景,最简单的方法就是使用beanshell前置处理对入参进行签名处理后再进行接口请求。
这种方法我之前也写过博客进行记录 jmeter 引用第三方jar包进行业务操作。
虽然解决了问题,但这样的实现方式还有一些弊端。
beansell 前置处理的缺陷:
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.xxxx.xx.security.RSAUtil;
import java.util.Random;
Long timestamp = System.currentTimeMillis();
vars.putObject("timestamp", timestamp);
vars.putObject("total_berth_num", 21170);
vars.putObject("open_berth_num", 2700);
vars.putObject("parking_code", "xxxx");
vars.putObject("free_berth_num", new Random().nextInt(2000));
vars.putObject("upload_time", "2021-03-19 16:28:33");
JSONObject jsonObject = new JSONObject(true);
jsonObject.put("acxx_id", "xxx");
jsonObject.put("timestamp", timestamp);
JSONObject biz_context = new JSONObject(true);
biz_context.put("open_xxx_num", vars.getObject("open_xxx_num"));
biz_context.put("open_xxx_num", vars.getObject("open_xxx_num"));
biz_context.put("paxxxx_code", vars.getObject("paxxxx_code"));
biz_context.put("frexxxrth_num", vars.getObject("frexxxrth_num"));
biz_context.put("upload_time", vars.getObject("upload_time"));
jsonObject.put("biz_context", biz_context);
String content = JSON.toJSONString(jsonObject);
String privateKey = "MIICdQIBADANBgkqhkiG9w0BAQEFAASC7Ftq";
//参数签名
String sign = RSAUtil.sign(content,privateKey);
log.info(content);
log.info(sign);
jsonObject.put("sign", sign);
vars.put("sign", sign);
log.info(JSON.toJSONString(jsonObject));
上面是beansell前置处理的代码,可以看到将所有参数都加入了jmeter的环境变量并实例化了一个JSONObject,对于每个参数我们都要添加到这个对象中,然后写死privateKey ,使用导入的jar包对参数进行签名,而我们的http取样器中也要将入参的值写成对应变量。
回到业务,这种接入数据接口是一系列接口,并不是只有1个。每个接口的参数也是不一样的,参数多的接口有20个左右的参数,如果我们每个接口在压测时都要进行变量和参数的构造会造成工作时间的浪费,并且参数和签名操作每个beansell都写一份,后期也不好维护。
对于beansell前置处理的缺陷的处理方式是将可复用和有共性的内容抽离出来进行封装。
这个需求可以通过jmeter的自定义java请求进行实现
新建一个maven项目,pom.xml文件需要引入ApacheJMeter_core
和ApacheJMeter_java
<dependency>
<groupId>org.apache.jmetergroupId>
<artifactId>ApacheJMeter_coreartifactId>
<version>5.0version>
dependency>
<dependency>
<groupId>org.apache.jmetergroupId>
<artifactId>ApacheJMeter_javaartifactId>
<version>5.0version>
dependency>
新建一个类实现jmeter的JavaSamplerClient
,实现接口的setupTest
runTest
teardownTest
getDefaultParameters
方法,这几个方法对应java请求的几个生命周期,从名字可以看出它们分别是执行前、执行中、执行后和获取入参(类加载)时运行,主要的业务操作是在runTest
中。
@Data
public class SignRequest implements JavaSamplerClient {
//请求地址
private static final String URL = "URL";
private static final String DEFAULT_URL = "";
//请求体
private static final String REQUEST_DATA = "REQUEST_DATA";
private static final String DEFAULT_REQUEST_DATA = "";
//需要生成随机数的参数
private static final String DYNAMIC_PARAMETERS = "DYNAMIC_PARAMETERS";
private static final String DEFAULT_DYNAMIC_PARAMETERS = "";
//私钥
private static final String PRIVATE_KEY = "PRIVATE_KEY";
private static final String DEFAULT_PRIVATE_KEY = "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAIc1KmG5mg2xuD7aZRmx/lzQzkAXR+9jiQ0HlgRDTkn/kz7yV/T6vv86LURSUeXyvS0F10aKkAPBTOm6gYiSqyzsz+Ev4y15KVEAVAShnksjBAkBJ8frUI0KqAsU01FDL8aW3hXMAp920bJaUDqqd+atmmWXrlC7nN+mQnw8ZvVd7vwVkZs/784HOKtnioof5p5M5AkAW0Q4u7Nbr6FC3nlrA/Si0SQde4FFMK1P9ehbAIemf69ddgNvIuV7ojn/uott8ELTG6+wU2p19ltqEhZat7Ftq";
//acccess_id
private static final String ACCESS_ID = "ACCESS_ID";
private static final String DEFAULT_ACCESS_ID = "A00002";
//用户输入的请求地址
private String inputUrl;
//用户输入的请求参数
private String inputRequestBody;
//用户输入的access_id
private String inputAccessId;
//用户输入的私钥
private String inputPrivateKey;
//用户输入的动态参数,字符串类型,逗号分割
private String inputDynamicParameters;
//签名结果
private String sign;
private SampleResult sampleResult = new SampleResult();
public void setupTest(JavaSamplerContext javaSamplerContext) {
//获取用户输入的参数
this.inputUrl = javaSamplerContext.getParameter(URL);
this.inputRequestBody = javaSamplerContext.getParameter(REQUEST_DATA);
this.inputAccessId = javaSamplerContext.getParameter(ACCESS_ID);
this.inputPrivateKey = javaSamplerContext.getParameter(PRIVATE_KEY);
this.inputDynamicParameters = javaSamplerContext.getParameter(DYNAMIC_PARAMETERS);
}
public SampleResult runTest(JavaSamplerContext javaSamplerContext) {
//构造请求体
JSONObject signObject = JSONObject.parseObject(this.inputRequestBody);
signObject.put("timestamp",System.currentTimeMillis());
signObject.put("access_id",this.inputAccessId);
//入参处理,部分参数按需求替换为动态参数
JSONObject biz_context = ParamterDeal.dynamicParametersDeal(this.inputDynamicParameters, (JSONObject) signObject.get("biz_context"));
signObject.put("biz_context", biz_context);
//签名前
String content = JSON.toJSONString(signObject);
//参数签名
try {
this.sign = RSAUtil.sign(content,this.inputPrivateKey);
} catch (Exception e) {
e.printStackTrace();
}
signObject.put("sign",this.sign);
//签名后
content = JSON.toJSONString(signObject);
//进行接口请求
HttpPost post = new HttpPost(this.inputUrl);
StringEntity entity = new StringEntity(content, "utf-8");
entity.setContentType("application/json");
entity.setContentEncoding("UTF-8");
post.setEntity(entity);
post.addHeader("Connection","keep-alive");
post.addHeader("Content-Type","application/json");
post.addHeader("User-Agent","Apache-HttpClient/4.5.9 (Java/1.8.0_151)");
HttpClient client = HttpClients.custom().setDefaultRequestConfig(RequestConfig.custom().setSocketTimeout(10000).setConnectTimeout(10000).setConnectionRequestTimeout(2000).build()).build();
HttpResponse response = null;
String result = "";
try {
//事务开始
this.sampleResult.sampleStart();
response = client.execute(post);
// 事务结束
this.sampleResult.sampleEnd();
result = EntityUtils.toString(response.getEntity(), "utf-8");
} catch (IOException e) {
e.printStackTrace();
}
JSONObject jsonResult = JSONObject.parseObject(result);
Header[] responseAllHeaders = response.getAllHeaders();
StringBuilder responseHeader = new StringBuilder();
for(Header header : responseAllHeaders){
String s = header.getName() + ": " + header.getValue() + "\n";
responseHeader.append(s);
}
Header[] requestAllHeaders = post.getAllHeaders();
StringBuilder requestHeader = new StringBuilder();
for(Header header : requestAllHeaders){
String s = header.getName() + ": " + header.getValue() + "\n";
requestHeader.append(s);
}
//构造jmeter响应结果
this.sampleResult.setResponseData(result, "utf-8");
this.sampleResult.setDataType(SampleResult.TEXT);
this.sampleResult.setResponseHeaders(responseHeader.toString());
this.sampleResult.setRequestHeaders(requestHeader.toString());
this.sampleResult.setResponseCode(String.valueOf(response.getStatusLine().getStatusCode()));
this.sampleResult.setSuccessful(response.getStatusLine().getStatusCode()==200 && Integer.parseInt(jsonResult.get("result_code").toString())==200);
try {
this.sampleResult.setSamplerData(Formatting.jsonFormat(new BufferedReader(new InputStreamReader(entity.getContent()))
.lines().collect(Collectors.joining("\n"))));
} catch (IOException e) {
e.printStackTrace();
}
return this.sampleResult;
}
public void teardownTest(JavaSamplerContext javaSamplerContext) {
}
/**
* 在jmeter中显示哪些属性,类加载得时候运行
*/
public Arguments getDefaultParameters() {
Arguments arguments = new Arguments();
arguments.addArgument(URL,DEFAULT_URL);
arguments.addArgument(REQUEST_DATA,DEFAULT_REQUEST_DATA);
arguments.addArgument(PRIVATE_KEY,DEFAULT_PRIVATE_KEY);
arguments.addArgument(ACCESS_ID,DEFAULT_ACCESS_ID);
arguments.addArgument(DYNAMIC_PARAMETERS,DEFAULT_DYNAMIC_PARAMETERS);
return arguments;
}
}
这里主要描述一下runTest
中的逻辑,获取用户输入的入参和接口地址,对入参进行参数构造,对参数进行签名,将签名好的入参和参数使用httpclient
进行请求,最后将请求结果返回并根据返回构造jmeter响应结果。注意,在压力测试时,发出的http请求是这块代码里面写的,并非jmeter取样器。
该服务构建jar包后将jar包放到jmeter安装目录下的lib\ext目录下重启jmeter即可。新增java请求,可见如下图新增的java类。
填写原始入参和接口请求地址即可,由于定义了jmeter结果和事务,所以依然可以通过查看结果树和聚合报告来进行结果查看和调试。
可以看到,使用自定义java请求相比较于beanshell前置处理会方便很多,每个接口不用写大段脚本来构建参数,只需要传入原始入参即可。并且很多重复的逻辑也不用每次都写一遍,极大的提高了脚本编写的效率。
自定义java请求的实现方式是否会有缺陷和问题:
实现了我们想要的功能并完成了压力测试,那么想一下这种方式是否会有问题。有的人一定想说,既然是性能测试,那么进行http请求这一步就至关重要,此处的http请求不是jmeter原生的而是你自己写的,那在性能结果上会不会有差异呢?
差异一定是有的,即使是一摸一样的脚本和环境,每次测试的测试结果也会有不同。但此处进行http请求和jmeter自己的http请求的差距应该是不大的,通过查看结果树可以看到,jmeter自己进行http请求使用的也是apache的HttpClient
,只是版本不同。且经过测试,2种方式的测试结果并无较大差距。
尽管进行过验证,但为了保障性能测试结果,仍然想使用jmeter自己的http取样器还想像自定义java请求一样方便的实现我们的签名需求应该怎么做呢?
在性能测试时,都有使用过jmeter自带的函数助手,比如参数化、随机数这种功能,那对于参数签名这个需求,也可以自定义一个函数助手来实现。
pom.xml文件引入ApacheJMeter_functions
<dependency>
<groupId>org.apache.jmetergroupId>
<artifactId>ApacheJMeter_functionsartifactId>
<version>5.0version>
dependency>
在项目目录下新增包fuctions
,一定要命名为fuctions
。
包下新增类继承jmeter的AbstractFunction
实现方法execute
setParameters
getReferenceKey
getArgumentDesc
,其中execute
主要实现逻辑。
public class SignFunction extends AbstractFunction {
//接收的参数数组
private Object[] values;
//私钥
private static final String PRIVATE_KEY = "MIICdQIBADANBgkqZat7Ftq";
//acccess_id
private static final String ACCESS_ID = "A00002";
@Override
public String execute(SampleResult sampleResult, Sampler sampler) throws InvalidVariableException {
//定义的参数 多个参数为 a,b,c
CompoundVariable requestParamter = (CompoundVariable) values[0];
String requestString = requestParamter.execute().trim();
CompoundVariable dynamicParameters = (CompoundVariable) values[1];
String dynamicString = dynamicParameters.execute().trim();
//构造请求体
JSONObject signObject = JSONObject.parseObject(requestString);
signObject.put("timestamp",System.currentTimeMillis());
signObject.put("access_id",ACCESS_ID);
//入参处理,部分参数按需求替换为动态参数
JSONObject biz_context = ParamterDeal.dynamicParametersDeal(dynamicString, (JSONObject) signObject.get("biz_context"));
signObject.put("biz_context", biz_context);
//签名前
String content = JSON.toJSONString(signObject);
//参数签名
String sign = null;
try {
sign = RSAUtil.sign(content,PRIVATE_KEY);
} catch (Exception e) {
e.printStackTrace();
}
signObject.put("sign",sign);
//签名后
content = Formatting.jsonFormat(JSON.toJSONString(signObject));
return content;
}
/**
* 接受用户传递的参数
* @param collection
* @throws InvalidVariableException
*/
@Override
public void setParameters(Collection<CompoundVariable> collection) throws InvalidVariableException {
checkParameterCount(collection,2);
this.values = collection.toArray();
}
/**
* 功能名称
* @return __signCCIParking
*/
@Override
public String getReferenceKey() {
return "__signCCIParking";
}
/**
* 参数描述
* @return desc
*/
@Override
public List<String> getArgumentDesc() {
List<String> desc = new ArrayList<>();
desc.add("需要签名的入参,返回为入参和签名的json串");
desc.add("需要动态改变的参数名称,用逗号分割");
return desc;
}
}
这个函数助手的功能是将用户输入的参数进行签名,然后将签名后的参数进行返回。进去接口请求时将签名后的输入参数传入,这样进行http请求的仍然是jmeter http取样器。
至此,我们已经有了3种方式来进行签名接口的性能测试
对于这个业务需求、对于开源的性能测试工具jmeter 我们还能怎么做,或者说我们还想怎么做?
我想在http取样器的基础上实现一个自定义的http取样器,这个取样器有一个签名开关的功能,开启开关对参数签名,关闭开关则不签名。可以在源码的基础上构建,也可以作为一个jar包插件导入。
就像下面这样
这是我修改jmeter源码,本地运行启动的jmeter。
想要实现的这个功能已经完成了一部分,如图中显示的,已经有了一个是否进行参数签名的开关,但仅仅只是UI界面的展示,这个开关的逻辑还没有实现。
目前对我来说,实现这个功能仍然需要研究,主要需花费时间在读懂jmeter源码上。如果之前有读过jmeter源码的小伙伴欢迎一起交流。
-------------------------------------------------------------------------------2021.11.15-----------------------------------------------------------------------
今天完成了这个在http采样器基础上增加一个签名开关的功能。
这里简单分享下思路,如何写一个有GUI界面的sample。首先对于开源可扩展软件,一定要看它的官网,既然支持插件扩展,那么官网一定会给出方法。
这里描述了一个sample类型的插件,主要有2方面的构成,一个是GUI界面,一个是采样器。
GUI的实现是继承AbstractSamplerGui
类并重写父类的一些方法。
/**gui显示的sample的名称**/
public String getStaticLabel()
public String getLabelResource()
//这个方法用于把界面的数据移到Sampler中。
public void modifyTestElement(TestElement testElement)
//界面与Sampler之间的数据交换
public void configure(TestElement el)
//该方法会在reset新界面的时候调用,这里可以填入界面控件中需要显示的一些缺省的值(就是默认显示值)
public void clearGui()
//该方法创建一个新的Sampler,然后将界面中的数据设置到这个新的Sampler实例中。
public TestElement createTestElement()
我这里的需求是在http采样器上做改造,所以2个类分别继承了UrlConfigGui
和AbstractSamplerGui
,新增了是否参数签名的checkbox,通过重写configure和modifyTestElement方法完成和sample之间的数据交互。
采样器的实现理论上继承AbstractSampler
类并重写sample
方法
//该方法是JMeter实现对目标系统发起请求实际工作的地方
public SampleResult sample(Entry entry)
这里本来想继承http采样器的HTTPSamplerProxy
类,但这个类是final,应该是jmeter认为这个类不应该也不允许被扩展,所以这里继承了HTTPSamplerBase
类,复制了HTTPSamplerProxy
里面的一些代码,在sample里面实现签名核心逻辑。
// 判断是否需要参数签名
if (this.hasArguments() && isSign){
Arguments arguments = this.getArguments();
// 签名前原始参数
String content = arguments.getArgument(0).getValue();
logger.info(content);
// 替换时间戳和access_id
JSONObject signObject = JSONObject.parseObject(content);
signObject.put("timestamp",System.currentTimeMillis());
signObject.put("access_id",ACCESS_ID);
content = JSON.toJSONString(signObject);
logger.info(content);
String sign = null;
try {
// 参数签名
sign = RSAUtil.sign(content,PRIVATE_KEY);
} catch (Exception e) {
e.printStackTrace();
}
logger.info("+++++++++++++++++++++++++++++++++++++++++++++++++++++++");
logger.info("sign:" + sign);
logger.info("+++++++++++++++++++++++++++++++++++++++++++++++++++++++");
signObject.put("sign", sign);
// 构造签名后参数
HTTPArgument argumentSign = new HTTPArgument();
argumentSign.setValue(JSON.toJSONString(signObject));
arguments.removeAllArguments();
arguments.addArgument(argumentSign);
this.setArguments(arguments);
logger.info(arguments.getArgument(0).getValue());
}
这里只对入参Arguments
进行修改,将入参取出签名后再传入arguments中,其他的都不做改动,这样尽可能保证除签名外所有功能和http采样器保持一致。
以下是目录结构,红框内容为此次新增核心功能代码,其余模块是由于类无法继承或相关方法为protect导致子类无法调用的原因从源码中拷贝并进行部分修改的,如果是直接在源码中编写则此类内容无需新增。
打包后添加jar到jmeter并启动运行
此次完成这个功能,主要从两方面入手。一方面是看官方文档和网上资料,另一方面就是看源码,仍然看的不是太懂,但既然是改造,照猫画虎,复制粘贴还是可以继续下去的。功能最终实现,即使对代码层面的很多内容还是不理解,但总归是有了一些进步,继续下去,一定会有更大的收获。