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

实现客户端

现在我们可以实现我们的网页浏览器客户端。

我们首先加载我们的证书及其私钥,并创建 QSslCertificateQSslKey 实例。

    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::certificateErrorQWebEnginePage::selectClientCertificate连接。第一个信号只需要,因为我们的自签名服务器证书将触发证书错误,我们需要接受错误以继续进行身份验证。在生产环境中不使用自签名证书,因此在这个例子中,我们仅处理QWebEngineCertificateError以避免提供适当的证书。注意私钥是保密的,绝不能公开发布。

    QWebEnginePage page;
    QObject::connect(&page, &QWebEnginePage::certificateError,
                     [](QWebEngineCertificateError e) { e.acceptCertificate(); });

QWebEnginePage::selectClientCertificate的处理简单显示带有QDialogQListWidget的对话框,其中显示可供选择的客户证书列表。然后,将用户选择的证书传递给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的实例,就像客户端设置一样,我们加载服务器证书及其私钥。接下来,我们创建相应的QSslCertificateQSslKey对象。此外,我们还需要一个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消息和一个ExitHTML按钮。如果管理员点击它,客户端将发送另一个请求。这一次,它使用带有相对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页面。

示例项目 @ code.qt.io

© 2024 Qt公司有限公司。包含在此处的文档贡献是该拥有者的版权。提供的文档受GNU自由文档许可1.3版本的条款所管理,由自由软件基金会发布。Qt及其标志是芬兰和/或其他国家的Qt有限责任公司商标。所有其他商标均为其各自所有者的财产。