编者注:本文转载自微信公众号“龙湖外环31号”,作者为狄雨晨。
开始之前
实施此方案需要熟悉下列技术栈
• JMeter使用及参数配置
• Java及JVM参数
• Linux操作
• Docker及Docker-compose原理和描述文件编写及调试
• MeterSphere平台操作
背景
某系统进行安全性提升改造,将原http暴露的服务通过应用网关代理后,对外统一为https方式。即外部请求首先走https方式到应用网关,由应用网关进行负载均衡,并将请求以http方式转发到具体应用后台,如下图所示。
其中,应用网关提供了PFX格式的签名证书,并提供了密码。
PFX格式说明
公钥加密技术12号标准(Public Key Cryptography Standards #12,PKCS#12)为存储和传输用户或服务器私钥、公钥和证书指定了一个可移植的格式。它是一种二进制格式,这些文件也称为PFX文件。开发人员通常需要将PFX文件转换为某些不同的格式,如PEM或JKS,以便可以为使用SSL通信的独立Java客户端或WebLogic Server使用。使用该协议,用户就可以安全地将个人信息从一个计算机系统导出到另一个系统中。
目标
在MeterSphere上配置应用网关需要的SSL证书,并发起https请求到应用网关,从而模拟前端应用发起的请求,进行接口自动化测试。
这里笔者多提一下,一般来说我们建议,除非是专门测试网络拓扑及部署规则,否则在测功能或者自动化的时候,离被测系统越近越好。但可能有部分Bug是就是由于网络拓扑原因所导致的,关注此部分的话应该在测试设计时考虑,此处不再展开讨论。针对此文的场景,应该绕过应用网关直接向后端服务发起http请求,但现在尚无有效的绕过方案,因此选择从应用网关外发起请求。
资料查询
考虑到MeterSphere使用的是JMeter引擎,经过搜索,得知有三种办法可以配置SSL证书,但似乎并不支持PFX格式。这里笔者没有认真求证,仅参考了部分搜索结果后将使用JDK自带的keytools将PFX证书转为了P12格式。
三种配置方法
1. 可以通过JMeter的SSL证书管理器配置证书;
2. 通过在<jmeter安装路径>/bin/system.properties</jmeter安装路径>文件中配置如下内容:
javax.net.ssl.keyStore=证书路径
javax.net.ssl.keyStorePassword=证书密码
3.在启动JMeter时追加JVM参数 -Djavax.net.ssl.keyStore=证书路径 -Djavax.net.ssl.keyStorePassword=证书密码
考虑到MeterSphere是以第三方包依赖方式引用的Jmeter,即通过代码方式调用Jmeter的执行引擎:
参见 Github上的LocalRunner类代码:
核心代码:
截至当前的v1.7.3版本,MeterSphere并未提供SSL证书管理器相关功能,因此该方案一行不通,开始考虑方案二和方案三。最终方案三获得成功,不关注探索过程的读者可直接阅读实际操作的方案三部分。
探索之旅
接下来是实操环节,无论采用哪种方案,均需要进行准备环节。
准备工作
无论那种操作,都需要解决“如何让MeterSphere能够读取到证书文件?”的问题。考虑到MeterSphere是运行在Docker容器中的,首先登录MeterSphere部署服务器,并执行msctl status查看容器信息:
可以看到ms-server容器正在运行中,进入此容器探索一番。
[root@metersphere ~]# docker exec -it ms-server sh
现在已经进入到ms-server的容器中了,各个文件夹逐个过一遍并不是个好主意,不如看看官方的Dockerfile吧,Dockerfile会提供很多信息
https://github.com/metersphere/metersphere/blob/v1.7/Dockerfile
文件内容如下:
果不其然,后端应用部署目录在/opt/apps下,JMeter的配置路径在/opt/jmeter下。方便起见,笔者将需要使用的证书放在了/opt/jmeter/bin目录下,和properties文件在同路径。
如何把证书文件放在容器内呢?看了下dockerfile,使用的是基于Alpine 上构建的Java运行环境镜像fabric8-java-alpine-openjdk8-jre,没有scp指令,但有wget指令。
Alpine系统简介
Alpine 操作系统是一个面向安全的轻型Linux发行版。它不同于通常的Linux发行版,Alpine 采用了musl libc和busybox以减小系统的体积和运行时资源消耗,但功能上比busybox又完善得多,因此得到开源社区越来越多的青睐。
在保持瘦身的同时,Alpine还提供了自己的包管理工具apk,可以通过 https://pkgs.alpinelinux.org/packages 网站上查询包信息,也可以直接通过apk命令直接查询和安装各种软件。但由于笔者的环境是私有化环境,与互联网完全隔离,在没有配置私有apk源的情况下,不能安装三方包。
关于如何将文件放在容器内方法有很多,这里不再展开讨论。
于是将需要导入的证书文件放置在一个Web服务器的文档路径下,使其可以通过http方式下载,然后在容器中使用wget将文件下载到容器内:
至此,全部准备工作完毕。
方案二
由于已经注意到了properties文件存在,因此笔者优先选择了方案二,验证后证明此方案不通,但还是留做记录。
需要注意的是,笔者在自己Windows电脑上的JMeter 5.4.1版本的安装目录/bin/下按照方案二进行了配置,确实是生效的,但是在容器中配置并不生效(也重启过了),官方引用的JMeter版本是5.2.1。针对这个疑问,笔者也咨询了官方,回答说理论上来说这个配置文件应该是生效的,因此方案二是否可行仍然存疑,欢迎感兴趣的读者进一步探究。
1. 修改system.properties文件
JMeter的properties文件位置在ms-server容器内的/opt/jmeter/bin目录下,按照上文搜集的资料创建system.properties并配置了文件内容。修改好后,保存文件。
2. 重启服务后验证效果
退出容器并使用msctl重启服务。
打开MeterSphere平台,并执行接口自动化场景,查看响应体,返回了错误代码901。说明证书配置并未生效。
紧接着查看控制台,看来确实没有生效。之后又多次调整properties文件,尝试过将上述配置写入到各个文件中并重启,均无法正确加载,因此放弃方案二,改为方案三。
方案三
方案二不通,只能选择方案三了,方案三的关键在于,如何让应用程序接受到上文中提到的keyStore这两个相关参数。
1. 寻找注入点
采用注入JVM参数方式进行配置。在Java应用程序启动时,通过命令行指定 -D<propertyName>=value ,可以在虚拟机的系统属性中设置属性名/值对,运行在此Java虚拟机之上的应用程序就可以用到。考虑到MeterSphere采用代码调用方式来调用JMeter的执行功能,因此只要给backend-1.7.jar注入JVM参数,则JMeter自然能够使用此参数,毕竟都在同一个JVM进程中。
首先进入容器,查看当前容器中的Java进程:
可以注意到,此时backend-1.7.jar正在运行,但并没有JMeter的运行进程,刚才上文也提到了,MeterSphere是以代码依赖方式调用的JMeter,因此JMeter就已经包含在了backend-1.7.jar中。此时,想要调整启动命令,就只能杀掉重启(对调整运行时JVM参数的方式本文不讨论,欢迎读者自行尝试)。但是,Java进程是这个容器的init进程,也就是1号进程,并不能杀掉。看看Dockerfile,果不其然,最后一行指定了:
CMD ["/deployment/run-java.sh"]
那就去/deployment/路径下查看一下这个run-java.sh到底写了啥,打开一看,有点复杂,感兴趣的读者可以自行尝试修改此文件内容。
此路不通,继续看Dockerfile,注意到倒数第二行指定了环境变量JAVA_OPTIONS,内容如下:
ENV JAVA_OPTIONS="-Dfile.encoding=utf-8 -Djava.awt.headless=true"
如果直接往变量里面注入追加的内容是否可以呢?可以是可以,但是应用程序已经启动了,此时修改变量无济于事。
自己修改Dockerfile并重新打包也是一种方案,但笔者觉得麻烦并没有尝试,感兴趣的读者可以在自己的环境动手试一下,这里不再展开讨论。
既然Docker不行,那就考虑docker-compose,毕竟msctl脚本是docker-compose的简化版,相当于为MeterSphere定制了docker-compose的运维指令。由于msctl指令在MeterSphere部署服务器上任意目录均可使用,说明此指令一定在PATH中,那就用which指令找一下:
vim打开/usr/local/bin/msctl,看看脚本写了啥?这里仅截取片段内容。
目前全部都指向${MS_BASE}/metersphere路径,也就是/opt/metersphere,去此路径下看看:
这就找到了docker-compose需要的yml描述文件了,使用vim打开docker-compose-server.yml看看:
version: "2.1"
services:
ms-server:
image: ${MS_PREFIX}/metersphere:${MS_TAG}
container_name: ms-server
environment:
HOST_HOSTNAME: $HOSTNAME
SPRING_DATASOURCE_URL: jdbc:mysql://${MS_MYSQL_HOST}:${MS_MYSQL_PORT}/${MS_MYSQL_DB}?autoReconnect=false&useUnicode=true&characterEncoding=UTF-8&characterSetResults=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=false
SPRING_DATASOURCE_USERNAME: ${MS_MYSQL_USER}
SPRING_DATASOURCE_PASSWORD: ${MS_MYSQL_PASSWORD}
KAFKA_PARTITIONS: 1
KAFKA_REPLICAS: 1
KAFKA_TOPIC: ${MS_KAFKA_TOPIC}
KAFKA_LOG_TOPIC: ${MS_KAFKA_LOG_TOPIC}
KAFKA_TEST_TOPIC: ${MS_KAFKA_TEST_TOPIC}
KAFKA_BOOTSTRAP-SERVERS: ${MS_KAFKA_EXT_HOST}:${MS_KAFKA_EXT_PORT}
Jmeter_IMAGE: ${MS_PREFIX}/Jmeter-master:${MS_Jmeter_TAG}
SESSION_TIMEOUT: 7200
ports:
- ${MS_PORT}:8081
healthcheck:
test: ["CMD", "nc", "-zv", "localhost", "8081"]
interval: 6s
timeout: 10s
retries: 20
volumes:
- ms-conf:/opt/metersphere/conf
- ms-logs:/opt/metersphere/logs
- ms-data:/opt/metersphere/data
mem_limit: 2048m
networks:
- ms-network#MS_EXTERNAL_MYSQL=false
depends_on:
mysql:
condition: service_healthy
其中Line7~Line18指定了应用内的环境变量,可以搞。
2. 进行变量注入
在ms-server的environment节点下增加了如下内容(写在了Line18行后的新行,遵循YAML语法,缩进与其他environment节点的参数一致),正好用来覆盖dockerfile中的JAVA_OPTIONS参数。
JAVA_OPTIONS: -Dfile.encoding=utf-8 -Djava.awt.headless=true -Djavax.net.ssl.keyStore=证书路径 -Djavax.net.ssl.keyStorePassword=证书密码
保存之后重新加载应用。
[root@metersphere metersphere]# msctl reload
待应用启动后,重新进入到容器中,查看参数注入是否生效?
[root@metersphere metersphere]# docker exec -it ms-server sh
已经生效了!
现在距离目标只剩下最后一步了,由于容器升级,原有的容器被销毁了,docker-compose创建了新的容器,于是使用wget重新下载了证书到新的容器内。此步骤只是为了把证书放到容器里,方式有很多,直接改docker-compose的数据卷挂载规则也可以,或者放在现有的数据卷下(具体参见docker-compose-server.ym的services.ms-server.volumes节点),这里笔者不再展开,注意调整过docker-compose的YAML文件后需要执行msctl reload (等价的:docker-compose up -d)来重新加载。
之后msctl restart重启容器,验证是否生效。
3.结果验证
重新运行接口测试,终于通了:
不放心,再看下控制台:
至此,问题完全解决。
总结
总的来说,问题的解决思路还是比较顺畅的,因为MeterSphere底层依赖的就是JMeter,那就尝试在MeterSphere上构造和JMeter一样的效果(一样的参数注入或者配置文件生效等)即可。
希望MeterSphere能够尽快支持SSL证书管理器相关功能,随着网络安全意识的提高,使用证书的场景只会更多,不会更少。相关需求已经提在Github的Issue上,参见“FEATURE增加ssl证书类型的支持 #1487”。