WebEngine Widgets 客户端证书示例
使用 Qt WebEngine 和 QSslServer 的一个简单的客户端证书认证场景。
在这个例子中,我们将展示客户端证书认证的工作流程。所提供的认证场景可以实现为一个嵌入式设备,该设备提供了一个网络界面来处理其功能。管理员使用由 Qt WebEngine 驱动的客户端来维护嵌入式设备,并有一个自定义的 SSL 证书进行认证。连接使用 SSL 套接字加密。嵌入式设备使用 QSslSocket
来处理认证和加密。这样管理员就不需要输入任何凭证,只需要选择设备识别的有效证书即可。
在本例中,我们关注于一个非常简单和简化的方法来展示工作流程。请注意,QSslSocket 是一个低级解决方案,因为在我们没有在资源有限的嵌入式设备上运行完整的 HTTPS 服务器的情况下。
创建证书
示例中已经生成了证书,但让我们看看如何生成新的。我们使用 OpenSSL 工具 为服务器和客户端创建证书。
首先,我们创建证书签名请求 CSR
并对其进行签名。我们将使用 CA 私钥来为客户端和服务器签名和颁发本地证书。
openssl req -out ca.pem -new -x509 -nodes -keyout ca.key
注意:指定 -days
选项以覆盖默认证书有效期 30 天。
现在,为我们的客户端和服务器创建两个私钥
openssl genrsa -out client.key 2048
openssl genrsa -out server.key 2048
接下来,我们需要两个证书签名请求
openssl req -key client.key -new -out client.req
openssl req -key server.key -new -out server.req
现在从 CSR 中颁发两个证书
openssl x509 -req -in client.req -out client.pem -CA ca.pem -CAkey ca.key
openssl x509 -req -in server.req -out server.pem -CA ca.pem -CAkey ca.key
客户端证书的主题和序列号将在认证过程中显示以供选择。可以使用以下代码打印序列号:
openssl x509 -serial -noout -in client.pem
实现客户端
现在我们可以实现我们的网页浏览器客户端。
我们首先加载我们的证书及其私钥,并创建 QSslCertificate 和 QSslKey 实例。
QFile certFile(":/resources/client.pem"); certFile.open(QIODevice::ReadOnly); const QSslCertificate cert(certFile.readAll(), QSsl::Pem); QFile keyFile(":/resources/client.key"); keyFile.open(QIODevice::ReadOnly); const QSslKey sslKey(keyFile.readAll(), QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey, "");
现在我们将证书及其私钥添加到 QWebEngineClientCertificateStore。
QWebEngineProfile::defaultProfile()->clientCertificateStore()->add(cert, sslKey);
要处理证书,我们需要创建一个QWebEnginePage的实例,并将两个信号QWebEnginePage::certificateError和QWebEnginePage::selectClientCertificate连接。第一个信号只需要,因为我们的自签名服务器证书将触发证书错误,我们需要接受错误以继续进行身份验证。在生产环境中不使用自签名证书,因此在这个例子中,我们仅处理QWebEngineCertificateError以避免提供适当的证书。注意私钥是保密的,绝不能公开发布。
QWebEnginePage page; QObject::connect(&page, &QWebEnginePage::certificateError, [](QWebEngineCertificateError e) { e.acceptCertificate(); });
QWebEnginePage::selectClientCertificate的处理简单显示带有QDialog和QListWidget的对话框,其中显示可供选择的客户证书列表。然后,将用户选择的证书传递给QWebEngineClientCertificateSelection::select调用。
QObject::connect( &page, &QWebEnginePage::selectClientCertificate, &page, [&cert](QWebEngineClientCertificateSelection selection) { QDialog dialog; QVBoxLayout *layout = new QVBoxLayout; QLabel *label = new QLabel(QLatin1String("Select certificate")); QListWidget *listWidget = new QListWidget; listWidget->setSelectionMode(QAbstractItemView::SingleSelection); QPushButton *button = new QPushButton(QLatin1String("Select")); layout->addWidget(label); layout->addWidget(listWidget); layout->addWidget(button); QObject::connect(button, &QPushButton::clicked, [&dialog]() { dialog.accept(); }); const QList<QSslCertificate> &list = selection.certificates(); for (const QSslCertificate &cert : list) { listWidget->addItem(cert.subjectDisplayName() + " : " + cert.serialNumber()); } dialog.setLayout(layout); if (dialog.exec() == QDialog::Accepted) selection.select(list[listWidget->currentRow()]); else selection.selectNone(); });
最后,我们为我们的QWebEnginePage创建一个QWebEngineView,加载服务器URL,并显示页面。
QWebEngineView view(&page); view.setUrl(QUrl("https://localhost:5555")); view.resize(800, 600); view.show();
实现服务器
对于我们嵌入式的设备,我们将开发一个最小化的HTTPS服务器。我们可以使用QSslServer来处理传入的连接并提供一个QSslSocket实例。为此,我们创建一个QSslServer的实例,就像客户端设置一样,我们加载服务器证书及其私钥。接下来,我们创建相应的QSslCertificate和QSslKey对象。此外,我们还需要一个CA证书,以便服务器可以验证客户端提供的证书。CA和本地证书设置为QSslConfiguration,并由服务器稍后使用。
QSslServer server; QSslConfiguration configuration(QSslConfiguration::defaultConfiguration()); configuration.setPeerVerifyMode(QSslSocket::VerifyPeer); QFile keyFile(":/resources/server.key"); keyFile.open(QIODevice::ReadOnly); QSslKey key(keyFile.readAll(), QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey); configuration.setPrivateKey(key); QList<QSslCertificate> localCerts = QSslCertificate::fromPath(":/resources/server.pem"); configuration.setLocalCertificateChain(localCerts); QList<QSslCertificate> caCerts = QSslCertificate::fromPath(":resources/ca.pem"); configuration.addCaCertificates(caCerts); server.setSslConfiguration(configuration);
接下来,我们将服务器设置为监听端口5555
的传入连接。
if (!server.listen(QHostAddress::LocalHost, 5555)) qFatal("Could not start server on localhost:5555"); else qInfo("Server started on localhost:5555");
我们为QTcpServer::pendingConnectionAvailable信号提供了一个lambda函数,其中我们实现了对传入连接的处理。这个信号在身份验证成功并且代码TLS加密开始后被触发。
QObject::connect(&server, &QTcpServer::pendingConnectionAvailable, [&server]() { QTcpSocket *socket = server.nextPendingConnection(); Q_ASSERT(socket); QPointer<Request> request(new Request); QObject::connect(socket, &QAbstractSocket::disconnected, socket, [socket, request]() mutable { delete request; socket->deleteLater(); });
上面使用的Request
对象是一个简单的QByteArray包装器,因为我们用QPointer帮助进行内存管理。该对象收集传入的HTTP数据。当请求完成或套接字被终止时,对象将被删除。
struct Request : public QObject { QByteArray m_data; };
对于请求的回复取决于请求的URL,并且以HTML页面的形式通过套接字发送回。对于GET
根请求,管理员看到Access Granted
消息和一个Exit
HTML按钮。如果管理员点击它,客户端将发送另一个请求。这一次,它使用带有相对URL的/exit
,这将依次触发服务器终止。
QObject::connect(socket, &QTcpSocket::readyRead, socket, [socket, request]() mutable { request->m_data.append(socket->readAll()); if (!request->m_data.endsWith("\r\n\r\n")) return; socket->write(http_ok); socket->write(html_start); if (request->m_data.startsWith("GET / ")) { socket->write("<p>ACCESS GRANTED !</p>"); socket->write("<p>You reached the place, where no one has gone before.</p>"); socket->write("<button onclick=\"window.location.href='/exit'\">Exit</button>"); } else if (request->m_data.startsWith("GET /exit ")) { socket->write("<p>BYE !</p>"); socket->write("<p>Have good day ...</p>"); QTimer::singleShot(0, &QCoreApplication::quit); } else { socket->write("<p>There is nothing to see here.</p>"); } socket->write(html_end); delete request; socket->disconnectFromHost(); });
要运行此示例,请先启动server
然后启动client
。在您选择证书后,将显示Access Granted
页面。
© 2024 Qt公司有限公司。包含在此处的文档贡献是该拥有者的版权。提供的文档受GNU自由文档许可1.3版本的条款所管理,由自由软件基金会发布。Qt及其标志是芬兰和/或其他国家的Qt有限责任公司商标。所有其他商标均为其各自所有者的财产。