QtWebApp开发https服务器,完成客户端与服务器基于ssl的双向认证,纯代码操作

引言:所谓http协议,本质上也是基于TCP/IP上服务器与客户端请求和应答的标准,web开发中常用的http server有apache和nginx。Qt程序作为http client可以使用QNetworkAccessManager很方便的进行http相关的操作。Qt本身并没有http server相关的库,也许是因为很少有这种需求吧。但是实际开发中也会有做简单的http server的需求。实际上QT6.4里面引入了基于http的服务器,但是看了下,也是非常的鸡肋。

大概有以下几个基于QT/C++的http服务器的库。

一、qthttpserver

前面说到,Qt官方在发行版中并没有提供相应的类来实现HTTP 服务端,但是Qt-lab中提供了一个qthttpserver,​ ​Github地址在这里​​,不知道基于什么原因Qt官方没有将其纳入正式的Qt发行版中,如果要使用qthttpserver的话,就得自己下载源码进行编译了,不过编译可没那么容易,会遇到各种错误,而且还没有提供说明文档,这就不太友好了。
关于编译或简单使用参考以下文章:

Qt开发的轻量级http服务器-QtHttpServer编译
关于Qt HttpServer的一些测试(Qt6.4.0rc)
QtHttpServer编译使用,亲测可用
QT基于qhttp-server搭建http服务器

二、cpp-httplib

这是Github上一个开源的C++实现http server的项目,使用方法非常简单,就只有一个头文件,引入到自己的项目中就可以使用了,不过这是一个多线程"阻塞"HTTP 库,如果您正在寻找"非阻塞"库,那就要换别的了。具体使用可以参考Github里面的文档介绍,很详细。​ ​

三、 QtWebApp

QtWepApp 是 C++ 中的 HTTP 服务器库,适用于 Linux、Windows、Mac OS 和Qt 框架支持的许多其他操作系统。QtWebApp要求Qt版本至少是 Qt5.15 及其以上
这个库使用也非常简单,并且很明显它是用QT写的,所以与Qt的项目可以无缝衔接。
但并不是QT官方写的,不过它的名气比Qt官方qthhttpserver库要大。

有趣的是,Qt制造商多年来一直在开发标准HTTP服务器,但到2022年,它仍然不包括在Qt库中。这也许可以解释为什么很多人使用的库。
实际上QT6.4里面引入了基于http的服务器,但是看了下,也是非常的鸡肋。

本次选择更为实用的QtWebApp来作为http服务器。

源码下载:http://www.stefanfrings.de/qtwebapp/index-en.html
Project homepage:http://stefanfrings.de/qtwebapp/index-en.html
Tutorial:http://stefanfrings.de/qtwebapp/tutorial/index.html
API doc:http://stefanfrings.de/qtwebapp/api/index.html

关于QtWebApp的使用,可以直接阅读源码提供的demo1,结合日志可以很快的理解。
更好的是直接参考QtWebApp的使用网站:Tutorial,真的是非常详细,,不过是英文版的,需要具备一定的英语阅读能力。网站如下图所示。


可以直接把源码模块直接引入到工程里面使用,源码里面就3个模块{httpserver、logging、templateengine},按需引入即可,,,httpserver模块是最核心的,肯定是要引入的。。。
也可以将源码编译成库,,然后再通过头文件+库的形式去倒腾,,,,
推荐直接引入源码,首先QtWebApp的模块并不大,,而且可以随时调试源码或修改源码,如下文陈述,就涉及到了修改源码。

四、双向认证

之前,使用nginx作为https服务器,与客户端进行了双向认证和单向认证。
参考以下两篇文章,本篇是在以下两篇文章的基础上继续开发的,,证书、私钥等的制作及流程验证在以下文章中均作了详细的说明。所以,阅读此篇文章前务必首先阅读以下两篇文章。

ssl单向证书和双向证书校验测试及搭建流程
QT充当客户端模拟浏览器等第三方客户端对https进行双向验证

此处,采用QtWebApp来手动开发https服务器,来完成与客户端之间基于ssl的双向认证
客户端:win10 + Qt5.9.9,192.168.64.1
服务端:win10 + Qt6.4, 192.168.64.176

此时直接将QtWebApp的demo1拷过来,并在配置文件中指定证书及秘钥,证书和秘钥在之前的文章中都倒腾过,并且直接使用之前做好的,可以参考ssl单向证书和双向证书校验测试及搭建流程一文。
并将配置文件的端口改为443,这是https的默认端口,,(当然也可以设置为其他端口,写为443时在客户端向服务器发起请求时无需手动指定端口号,否则必须手动指定端口号)

此时运行https服务,发现报错如下,,,,,,,说白了,要想支持https,Qt的版本至少是5.15。。

好,此时去虚拟机中安装Qt6.4。。
安装完毕后,,运行程序,此时报错缺少 core5compat模块。。。

于是,去Qt里添加 Qt 5 Compatibility Module组件,,

倒腾完成后,此时运行,一切OK。。接下来首先确保主机和虚拟机互ping通(注意关闭防火墙)。
结果访问 https://192.168.64.176 时,无任何响应。。
wireshark抓包观察,结果可以明显看出ssl并没有建立成功,client发出第一次握手clienthello时,服务端压根没有任何回应。。。。服务端连最基本的serverhello都没有回应。。


好,再通过curl工具(可以看作是客户端)看看,,可以看出curl访问时是一直在那儿等着服务端的回应。。
但服务端那边迟迟无响应,,最终握手失败。

bash 复制代码
curl -k --cert ./client.crt --key ./client.key --show-error  https://192.168.64.176 -v


ssl握手失败的情况比较多,,可以参考下两篇文章:https://zhuanlan.zhihu.com/p/572516140
https://www.jiyik.com/tm/xwzj/network_287.html ,说实话,此时你压根就不知道是哪个环节出了问题。。这两篇文章只能是大概看看,有点印象罢了。。。

好,我们来看看服务端的日志,,,

可以发现,实际上没有任何有用的信息。。
此时只能去阅读QtWebApp的源码。。= > 柳暗花明又一村。。。

因为https服务器的创建肯定是要加载服务端证书和私钥的,,那就只好去看看它是怎么加载的,,在httpconnectionhandlerpool.cpp 源码的140行处是加载服务端私钥的地方。因为之前在通过openssl创建服务端的私钥时指定了私钥的密码123456,而源码中通过QSSLKey创建秘钥时却没有指定密码,,,所以导致出现了客户端发起握手请求clienthello时,服务端无动于衷,,,实际上就是因为服务器加载私钥有误导致的。。。这里吐槽下,QtWebApp这地方处理的不好,,这种错误应该是要以日志的形式呈现给调试者的,,,,,最终导致握手失败。。。

修改完源码后,此时重新启动服务端程序,,,先用curl先测试一把,,

果然,此时握手已经成功了,客户端也接受了来自服务端的响应。
从此处我们也可以看出,,之前我们一开始通过curl工具握手失败时,分析查看了curl的相关提示,,,以及服务端QtWebApp的日志,都没有看出有价值的提示,,,所以这种情况下,还是需要去通过分析以及修改源码大法来倒腾的,,,

【注意】此处使用curl命令之前需要首先导入客户端p12格式的证书,可阅读此篇文章:ssl单向证书和双向证书校验测试及搭建流程 中的双向证书校验小节内容,此处不再赘述。。。
【值得注意的是】:在 [ssl单向证书和双向证书校验测试及搭建流程] 一文中使用curl工具验证时,并没有事先导入客户端p12格式的证书就直接验证了,也是可以的,当然了,那时的https服务器是基于linux下的nginx的此处验证时发现必须首先导入客户端p12格式的证书,否则直接响应连接失败:schannel: failed to receive handshake, SSL/TLS connection failed具体原因未知,,待后倒腾。。

好,抓包看看,客户端和服务器之间是否向彼此发送了证书。

答案是肯定的,,因为只有双向认证的情况下,客户端才会向服务端发送证书。

书归正传,此时,我们已经通过QtWebApp以代码的形式创建了https服务器,,
接下来,我们的客户端就要通过我们自己编写的代码去替代刚才的curl工具或浏览器这种集成的第三方客户端去向服务器发起https请求,,,

css 复制代码
void MainWindow::testSsl()
{
    // 加载客户端证书
    QFile crtFile("C:\\Users\\XingWei\\Documents\\untitled19\\client.crt");
    crtFile.open(QIODevice::ReadOnly);
    QSslCertificate certificate(&crtFile, QSsl::Pem);
    crtFile.close();

    // 加载客户端私钥
    QByteArray passPhrase("123456");  // 私钥的密码
    QFile keyFile("C:\\Users\\XingWei\\Documents\\untitled19\\client.key");
    keyFile.open(QIODevice::ReadOnly);
    QSslKey privateKey(&keyFile, QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey, passPhrase);
    keyFile.close();

    // ssl配置
    QSslConfiguration conf;
    conf.setPeerVerifyMode(QSslSocket::QueryPeer);
    conf.setProtocol(QSsl::TlsV1SslV3);
    conf.setPrivateKey(privateKey);
    conf.setLocalCertificate(certificate);

    QNetworkRequest request;
    request.setUrl(QUrl("https://192.168.64.176"));
    request.setSslConfiguration(conf);

    QEventLoop loop;
    QNetworkReply* reply = m_manager.post(request, QByteArray("AAAAAAAAAAAAAAAAAAAAA"));
    connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
    connect(reply, &QNetworkReply::finished, this, [=] {

        qDebug() << "####################### get data finished...";

        QByteArray data = reply->readAll();
        qDebug() << data;
    });
    loop.exec();

    qDebug() << "#############> " << reply->errorString() << reply->error();
}

运行效果及抓包如下所示:

客户端和服务端建立起ssl通道后就可以将数据加密交互了。。

好,接下来,我们在https服务器中拦截来自客户端的不同请求路径,进而做出相应的回应。
好,在客户端通过向服务器发送不同的请求路径,看看响应效果如何。。。
因为demo1中提供了几个请求路径,此处就以demo1提供的请求路径为基点来测试。。。
并在服务端新增一个/hello的请求路径,,该路径向客户端响应给以HELLO,SSL.字符串。

修改上述的客户端代码如下:

javascript 复制代码
// ...
QNetworkRequest request;
request.setUrl(QUrl("https://192.168.64.176/template"));
//    request.setUrl(QUrl("https://192.168.64.176/dump"));
//    request.setUrl(QUrl("https://192.168.64.176/hello"));
request.setSslConfiguration(conf);

QEventLoop loop;
QNetworkReply* reply = m_manager.get(request);
// ...

服务端新增HelloController请求处理类相关代码:

cpp 复制代码
void HelloController::service(HttpRequest &request, HttpResponse &response)
{
   response.setHeader("Content-Type", "text/html; charset=UTF-8");

   response.write("<html><body>");
   response.write("<h1>");
   response.write("HELLO, SSL.");
   response.write("</h1>");
   response.write("</body></html>",true);
}

访问/template路径时https服务器的响应:

javascript 复制代码
"<html><body>\r\n\r\nHello,<br>\r\nyou requested the path: /template\r\n<p>\r\nAnd your web browser provided the following headers:\r\n<p>\r\n\r\n  <b>accept-encoding:</b> gzip, deflate<br>\r\n\r\n  <b>accept-language:</b> zh-CN,en,*<br>\r\n\r\n  <b>connection:</b> Keep-Alive<br>\r\n\r\n  <b>host:</b> 192.168.64.176<br>\r\n\r\n  <b>user-agent:</b> Mozilla/5.0<br>\r\n\r\n\r\n</html></body>\r\n"

访问/dump时的响应:

kotlin 复制代码
"<html><body><b>Request:</b><br>Method: GET<br>Path: /dump<br>Version: HTTP/1.1<p><b>Headers:</b><br>accept-encoding=gzip, deflate<br>accept-language=zh-CN,en,*<br>connection=Keep-Alive<br>host=192.168.64.176<br>user-agent=Mozilla/5.0<p><b>Parameters:</b><p><b>Cookies:</b><p><b>Body:</b><br></body></html>"

访问/hello时的响应:

markup 复制代码
"<html><body><h1>HELLO, SSL.</h1></body></html>"

用浏览器来验证一下我们的请求,更加直观,如下所示:
【注意】:此处使用浏览器验证时,必须首先导入客户端p12格式的证书,否则是无法访问的。
关于客户端证书的导入,阅读此篇文章:ssl单向证书和双向证书校验测试及搭建流程中的双向证书校验小节内容,此处不再赘述。。。

由此可见,一切正常。
当https建立后,之前基于http写的请求路径以及对应的处理函数都不用作任何改变,直接向访问http那样访问就可以了,只不过此时需要变成https了。。
由此可见,https只不过是在http的请求基础上加了一层加密。

关于http的明文传输和https的加密传输,可以通过抓包来看。
可进一步阅读 QtWebApp同时开启http服务和https服务,接受来自客户端的不同请求并进行相应的处理 一文中 测试效果小节 所对应的内容。

相关推荐
叶北辰CHINA3 小时前
nginx反向代理,负载均衡,HTTP配置简述(说人话)
linux·运维·nginx·http·云原生·https·负载均衡
GodK7778 小时前
HTTPS 的加密流程
网络协议·http·https
一律清风10 小时前
QT-文件创建时间修改器
c++·qt
不知所云,10 小时前
qt cmake自定义资源目录,手动加载资源(图片, qss文件)
开发语言·qt
Death20011 小时前
Qt 6 相比 Qt 5 的主要提升与更新
开发语言·c++·qt·交互·数据可视化
机器视觉知识推荐、就业指导11 小时前
使用Qt实现实时数据动态绘制的折线图示例
开发语言·qt
Geek之路15 小时前
QT系统学习篇(1)
开发语言·qt·学习
我命由我1234516 小时前
SSL 协议(HTTPS 协议的关键)
网络·经验分享·笔记·学习·https·ssl·学习方法
中草药z1 天前
【JavaEE】http/https 超级详解
网络·笔记·网络协议·学习·http·https·计网
物有本木1 天前
httpsok-v1.17.0-SSL通配符证书自动续签
网络·网络协议·ssl