快速安全 CoAP 客户端

确保 CoAP 客户端的安全并将其用于Qt Quick 用户界面。

快速安全 CoAP 客户端演示了如何创建安全 CoAP 客户端并在Qt Quick 应用程序中使用。

注意: Qt CoAP 当前版本不提供 QML API。不过,您可以将模块的 C++ 类提供给 QML,如示例所示。

运行示例

要从 Qt Creator,打开Welcome 模式,从Examples 选择示例。更多信息,请参阅Qt Creator: Tutorial:构建并运行

要运行示例应用程序,首先需要设置安全 CoAP 服务器。您可以使用任何支持预共享密钥 (PSK)证书验证模式的安全 CoAP 服务器运行示例。有关设置安全 CoAP 服务器的更多信息,请参阅设置安全 CoAP 服务器

向 QML 公开 C++ 类

在本例中,您需要将QCoapClient 类和QtCoap 命名空间公开给 QML。为此,创建一个自定义封装类并使用特殊注册宏

创建QmlCoapSecureClient 类作为QCoapClient 的封装类。该类还保存所选的安全模式和安全配置参数。使用Q_INVOKABLE 宏向 QML 公开几个方法。还可使用QML_NAMED_ELEMENT 宏将该类在 QML 中注册为CoapSecureClient

class QmlCoapSecureClient : public QObject
{
    Q_OBJECT
    QML_NAMED_ELEMENT(CoapSecureClient)

public:
    QmlCoapSecureClient(QObject *parent = nullptr);
    ~QmlCoapSecureClient() override;

    Q_INVOKABLE void setSecurityMode(QtCoap::SecurityMode mode);
    Q_INVOKABLE void sendGetRequest(const QString &host, const QString &path, int port);
    Q_INVOKABLE void setSecurityConfiguration(const QString &preSharedKey, const QString &identity);
    Q_INVOKABLE void setSecurityConfiguration(const QString &localCertificatePath,
                                              const QString &caCertificatePath,
                                              const QString &privateKeyPath);
    Q_INVOKABLE void disconnect();

Q_SIGNALS:
    void finished(const QString &result);

private:
    QCoapClient *m_coapClient;
    QCoapSecurityConfiguration m_configuration;
    QtCoap::SecurityMode m_securityMode;
};

然后,注册QtCoap 命名空间,这样就可以使用那里提供的枚举:

namespace QCoapForeignNamespace
{
    Q_NAMESPACE
    QML_FOREIGN_NAMESPACE(QtCoap)
    QML_NAMED_ELEMENT(QtCoap)
}

调整构建文件

为使 QML 提供自定义类型,请相应更新构建系统文件。

CMake

对于基于 CMake 的联编,请将以下内容添加到CMakeLists.txt

qt_add_qml_module(quicksecureclient
    URI CoapSecureClientModule
    SOURCES
        qmlcoapsecureclient.cpp qmlcoapsecureclient.h
    QML_FILES
        FilePicker.qml
        Main.qml
)
qmake

对于 qmake 联编,按以下方式修改quicksecureclient.pro 文件:

CONFIG += qmltypes
QML_IMPORT_NAME = CoapSecureClientModule
QML_IMPORT_MAJOR_VERSION = 1
    ...
qml_resources.files = \
    qmldir \
    FilePicker.qml \
    Main.qml

qml_resources.prefix = /qt/qml/CoapSecureClientModule

RESOURCES += qml_resources

使用新的 QML 类型

现在,当 C++ 类正确暴露于 QML 时,你就可以使用新类型了。

创建客户端

CoapSecureClient 是由 文件实例化的。它处理 信号并相应地更新用户界面:Main.qml QmlCoapSecureClient::finished()

CoapSecureClient {
    id: client
    onFinished: (result) => {
        outputView.text = result;
        statusLabel.text = "";
        disconnectButton.enabled = true;
    }
}

当用户在用户界面中选择或更改安全模式时,就会创建QCoapClient 的实例。当选择其中一种安全模式时,QML 代码会调用QmlCoapSecureClient::setSecurityMode() 方法:

ButtonGroup {
    id: securityModeGroup
    onClicked: {
        if ((securityModeGroup.checkedButton as RadioButton) === preSharedMode)
            client.setSecurityMode(QtCoap.SecurityMode.PreSharedKey);
        else
            client.setSecurityMode(QtCoap.SecurityMode.Certificate);
    }
}

在 C++ 端,该方法会创建一个QCoapClient 并连接到finished() 和error() 信号。该类在内部处理这两个信号,并将它们转发到新的finished() 信号。

void QmlCoapSecureClient::setSecurityMode(QtCoap::SecurityMode mode)
{
    // Create a new client, if the security mode has changed
    if (m_coapClient && mode != m_securityMode) {
        delete m_coapClient;
        m_coapClient = nullptr;
    }

    if (!m_coapClient) {
        m_coapClient = new QCoapClient(mode);
        m_securityMode = mode;

        connect(m_coapClient, &QCoapClient::finished, this,
                [this](QCoapReply *reply) {
                    if (!reply)
                        emit finished(tr("Something went wrong, received a null reply"));
                    else if (reply->errorReceived() != QtCoap::Error::Ok)
                        emit finished(errorMessage(reply->errorReceived()));
                    else
                        emit finished(reply->message().payload());
                });

        connect(m_coapClient, &QCoapClient::error, this,
                [this](QCoapReply *, QtCoap::Error errorCode) {
                    emit finished(errorMessage(errorCode));
                });
    }
}
发送请求

单击Send Request 按钮,根据所选安全模式设置安全配置,并发送GET 请求:

Button {
    id: requestButton
    text: qsTr("Send Request")
    enabled: securityModeGroup.checkState !== Qt.Unchecked

    onClicked: {
        outputView.text = "";
        if ((securityModeGroup.checkedButton as RadioButton) === preSharedMode)
            client.setSecurityConfiguration(pskField.text, identityField.text);
        else
            client.setSecurityConfiguration(localCertificatePicker.selectedFile,
                                            caCertificatePicker.selectedFile,
                                            privateKeyPicker.selectedFile);

        client.sendGetRequest(hostComboBox.editText, resourceField.text,
                              parseInt(portField.text));

        statusLabel.text = qsTr("Sending request to %1%2...").arg(hostComboBox.editText)
                                                             .arg(resourceField.text);
    }
}

setSecurityConfiguration 方法有两种重载。

PSK 模式的重载只需设置客户端身份和预共享密钥:

void
QmlCoapSecureClient::setSecurityConfiguration(const QString &preSharedKey, const QString &identity)
{
    QCoapSecurityConfiguration configuration;
    configuration.setPreSharedKey(preSharedKey.toUtf8());
    configuration.setPreSharedKeyIdentity(identity.toUtf8());
    m_configuration = configuration;
}

而 X.509 证书的重载会读取证书文件和私钥,并设置安全配置:

void QmlCoapSecureClient::setSecurityConfiguration(const QString &localCertificatePath,
                                                   const QString &caCertificatePath,
                                                   const QString &privateKeyPath)
{
    QCoapSecurityConfiguration configuration;

    const auto localCerts =
            QSslCertificate::fromPath(QUrl(localCertificatePath).toLocalFile(), QSsl::Pem,
                                      QSslCertificate::PatternSyntax::FixedString);
    if (localCerts.isEmpty())
        qCWarning(lcCoapClient, "The specified local certificate file is not valid.");
    else
        configuration.setLocalCertificateChain(localCerts.toVector());

    const auto caCerts = QSslCertificate::fromPath(QUrl(caCertificatePath).toLocalFile(), QSsl::Pem,
                                                   QSslCertificate::PatternSyntax::FixedString);
    if (caCerts.isEmpty())
        qCWarning(lcCoapClient, "The specified CA certificate file is not valid.");
    else
        configuration.setCaCertificates(caCerts.toVector());

    QFile privateKey(QUrl(privateKeyPath).toLocalFile());
    if (privateKey.open(QIODevice::ReadOnly)) {
        QCoapPrivateKey key(privateKey.readAll(), QSsl::Ec);
        configuration.setPrivateKey(key);
    } else {
        qCWarning(lcCoapClient) << "Unable to read the specified private key file"
                                << privateKeyPath;
    }
    m_configuration = configuration;
}

设置安全配置后,sendGetRequest 方法会设置请求 URL 并发送GET 请求:

void QmlCoapSecureClient::sendGetRequest(const QString &host, const QString &path, int port)
{
    if (!m_coapClient)
        return;

    m_coapClient->setSecurityConfiguration(m_configuration);

    QUrl url;
    url.setHost(host);
    url.setPath(path);
    url.setPort(port);
    m_coapClient->get(url);
}

发送第一个请求时,要与 CoAP 服务器握手。握手成功后,所有后续信息都会加密,因此在握手成功后更改安全配置不会有任何影响。如果要更改或更换主机,需要先断开连接。

void QmlCoapSecureClient::disconnect()
{
    if (m_coapClient)
        m_coapClient->disconnect();
}

这将中止握手并关闭打开的套接字。

使用 X.509 证书进行身份验证时,需要指定证书文件。为此,需要使用FilePicker 组件。该组件由一个文本字段和一个按钮组成,按下按钮即可打开文件对话框:

Item {
    id: filePicker

    property string dialogText
    property alias selectedFile: filePathField.text

    height: addFileButton.height

    FileDialog {
        id: fileDialog
        title: qsTr("Please Choose %1").arg(filePicker.dialogText)
        currentFolder: StandardPaths.writableLocation(StandardPaths.HomeLocation)
        fileMode: FileDialog.OpenFile
        onAccepted: filePathField.text = fileDialog.selectedFile
    }

    RowLayout {
        anchors.fill: parent
        TextField {
            id: filePathField
            placeholderText: qsTr("<%1>").arg(filePicker.dialogText)
            inputMethodHints: Qt.ImhUrlCharactersOnly
            selectByMouse: true
            Layout.fillWidth: true
        }

        Button {
            id: addFileButton
            text: qsTr("Add %1").arg(filePicker.dialogText)
            onClicked: fileDialog.open()
        }
    }
}

FilePicker 该组件在 文件中被多次实例化,用于创建证书和私钥的输入字段:Main.qml

FilePicker {
    id: localCertificatePicker
    dialogText: qsTr("Local Certificate")
    enabled: (securityModeGroup.checkedButton as RadioButton) === certificateMode
    Layout.columnSpan: 2
    Layout.fillWidth: true
}

FilePicker {
    id: caCertificatePicker
    dialogText: qsTr("CA Certificate")
    enabled: (securityModeGroup.checkedButton as RadioButton) === certificateMode
    Layout.columnSpan: 2
    Layout.fillWidth: true
}

FilePicker {
    id: privateKeyPicker
    dialogText: qsTr("Private Key")
    enabled: (securityModeGroup.checkedButton as RadioButton) === certificateMode
    Layout.columnSpan: 2
    Layout.fillWidth: true
}

设置安全 CoAP 服务器

要运行此示例,需要有一个支持 PSK 或证书模式(或两者)的安全 CoAP 服务器。您有以下选项:

  • 使用libcoapCaliforniumFreeCoAP 或任何其他支持 DTLS 的 CoAP 库,手动构建并运行安全 CoAP 服务器。
  • 使用 Docker Hub 上现成的 Docker 镜像,这些镜像可构建并运行适合我们示例的安全 CoAP 服务器。使用基于 Docker 的 CoAP 服务器所需的步骤如下。
为 PSK 模式设置服务器

下面的命令会从 Docker Hub 中提取基于Californium plugtest的安全 CoAP 服务器(默认情况下不安全)的 docker 容器并启动它:

docker run --name coap-test-server -d --rm -p 5683:5683/udp -p 5684:5684/udp tqtc/coap-californium-test-server:3.8.0

CoAP 测试服务器的端口为5683(非安全)和5684(安全)。有关获取 IP 地址的说明,请参阅获取 IP 地址

要使用此服务器运行示例,需要将预共享密钥设置为secretPSK ,将身份设置为Client_identity

为证书模式设置服务器

使用 X.509 证书进行身份验证的安全服务器的 docker 镜像基于 FreeCoAP 库中的时间服务器示例。以下命令会从 Docker Hub 中提取容器并启动它:

docker run --name coap-time-server -d --rm -p 5684:5684/udp tqtc/coap-secure-time-server:freecoap

有关获取 IP 地址的说明,请参阅获取 IP 地址。通过检索到的 IP 地址,可以通过端口5684和资源路径/time 访问 CoAP 测试服务器。

要使用该服务器运行示例,需要指定服务器所需的证书文件。它们位于 docker 容器的/root/certs 目录下。要将它们复制到本地目录,请使用以下命令:

docker cp <container_id>:/root/certs <local_directory_path>

例如

$ docker cp 5e46502df88f:/root/certs ~/

获取容器 ID 的说明如下。

获取 IP 地址

要找出 docker 容器的 IP 地址,首先要运行docker ps 命令来获取容器 ID,其输出结果如下:

$ docker ps
CONTAINER ID        IMAGE
5e46502df88f        tqtc/coap-californium-test-server:3.8.0

然后,你可以用下面的命令获取 IP 地址:

docker inspect <container_id> | grep IPAddress

例如

$ docker inspect 5e46502df88f | grep IPAddress
...
"IPAddress": "172.17.0.2",
...
终止 Docker 容器

要在使用后终止一个 docker 容器,请使用以下命令:

docker stop <container_id>

这里的<container_id>docker ps 命令获取的相同。

文件:

© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.