阿里云IoT平台提供了基于MQTT的动态注册服务, 也叫一型一密, 可在设备运行时才分配deviceSecret或者MQTT建连参数clientId, userNamepassword, 以避免在产线上对每个设备烧写不同的设备密钥。MQTT形式的动态注册支持不用预先上传设备的DeviceName作为白名单。

API列表

以下是API列表及简要说明 (详见components/dynregmq/aiot_dynregmq_api.h)

接口名 说明
aiot_dynregmq_init 初始化dynregmq实例并设置默认参数
aiot_dynregmq_setopt 配置dynregmq实例, 详见dynregmq选项配置说明
aiot_dynregmq_deinit 释放dynregmq实例句柄的资源
aiot_dynregmq_send_request 向dynregmq服务器发送请求
aiot_dynregmq_recv 从dynregmq服务器接收应答

免白名单与不免白名单的区别

如果客户可以预先将设备的DeviceName上传到物联网平台做白名单,那么设备通过动态注册获取到的将是设备的DeviceSecret,然后设备再通过ProductKey、DeviceName和DeviceSecret连接物联网平台。设备获取到DeviceSecret之后需要将其持久化存储,设备重启、Reset都不能将DeviceSecret丢弃掉,否则设备无法再次连接物联网平台。

如果客户选择不预先上传设备的DeviceName到物联网平台做白名单,那么设备通过动态注册获取到的将是MQTT连接时使用的username、password、clientID,设备连接时需要将这三个参数传递到物联网平台完成设备认证。同样,设备获取到的username、password和clientID需要在设备上持久化存储。

警告 免白名单方案的安全性比白名单方案要低,如果产品的ProductKey、ProductSecret泄漏,会导致攻击者伪造设备连接到物联网平台,可能对客户的业务造成影响。因此:
  • 客户在设计时一定要将ProductKey、ProductSecret以某种加密方式存储在设备的Flash或者固件中,然后通过解密方式将其解密后使用,避免攻击者轻易获取到产品的ProductKey和ProductSecret。
  • 请务必集成“设备取证”功能,用于识别设备是否是伪造的设备。

例程讲解

现对照components/dynreg-mqtt/demos/dynregmq_basic_demo.c例程, 分步骤讲解如何使用API

这个例程演示了用SDK配置dynregmq会话实例的参数, 并发起请求和接收应答, 之后

  • 如果接收应答失败了, 销毁实例, 回收资源, 结束程序退出
  • 如果接收应答成功, 在demo_dynregmq_recv_handler()的应答处理回调函数中, 演示解析获取服务端应答的内容

需要用户关注或修改的部分, 已用 TODO 在注释中标明

设置产品信息与设备标识

例程中的ProductKey、ProductSecret、DeviceName是测试使用的, 客户在运行例程前请替换如下的TODO部分, 传入客户自己设备的真实参数。

   /* TODO: 替换为自己设备的productKey, productSecret和deviceName */
    char *product_key       = "a13FNxxxxx";
    char *product_secret    = "y7GSILxxxxxx";
    char *device_name       = "dynregmq_xxxxxxxxx";

进入程序入口, 给SDK配置全局的底层依赖和日志回调

底层依赖描述了硬件平台的资源使用方式, 比如怎样获取时钟, 分配内存等, 日志回调是用户的函数, SDK有log输出的时候会进入这个函数

int main(int argc, char *argv[])
{
    ...
    ...
    /* 配置SDK的底层依赖 */
    aiot_sysdep_set_portfile(&g_aiot_sysdep_portfile);
    /* 配置SDK的日志输出 */
    aiot_state_set_logcb(demo_state_logcb);
			

给动态注册会话配置参数

这些连接参数包括如何建立TLS连接, 服务器地址在哪, 连接后的数据及事件回调函数是什么等等

    /* 创建1个dynregmq客户端实例并内部初始化默认参数 */
    dynregmq_handle = aiot_dynregmq_init();
    if (dynregmq_handle == NULL) {
        printf("aiot_dynregmq_init failed\n");
        return -1;
    }

    /* 配置连接的服务器地址 */
    aiot_dynregmq_setopt(dynregmq_handle, AIOT_DYNREGMQOPT_HOST, (void *)host);
    /* 配置连接的服务器端口 */
    aiot_dynregmq_setopt(dynregmq_handle, AIOT_DYNREGMQOPT_PORT, (void *)&port);
    /* 配置设备productKey */
    aiot_dynregmq_setopt(dynregmq_handle, AIOT_DYNREGMQOPT_PRODUCT_KEY, (void *)product_key);
    /* 配置设备productSecret */
    aiot_dynregmq_setopt(dynregmq_handle, AIOT_DYNREGMQOPT_PRODUCT_SECRET, (void *)product_secret);
    /* 配置设备deviceName */
    aiot_dynregmq_setopt(dynregmq_handle, AIOT_DYNREGMQOPT_DEVICE_NAME, (void *)device_name);
    /* 配置网络连接的安全凭据, 上面已经创建好了 */
    aiot_dynregmq_setopt(dynregmq_handle, AIOT_DYNREGMQOPT_NETWORK_CRED, (void *)&cred);
    /* 配置DYNREGMQ默认消息接收回调函数 */
    aiot_dynregmq_setopt(dynregmq_handle, AIOT_DYNREGMQOPT_RECV_HANDLER, (void *)demo_dynregmq_recv_handler);
    /* 设置用户上下文,该上下文会在 demo_dynregmq_recv_handler 被调用时传回 */
    aiot_dynregmq_setopt(dynregmq_handle, AIOT_DYNREGMQOPT_USERDATA, (void *)&demo_info);
    /* 配置是否使用免白名单模式 */
    aiot_dynregmq_setopt(dynregmq_handle, AIOT_DYNREGMQOPT_NO_WHITELIST, (void *)&nowhitelist);
    ...
    ...
			

向服务器发送服务请求

使用aiot_dynregmq_send_request()接口, 以上面设置的参数, 向dynregmq服务器发起服务请求

    /* 发送动态注册请求 */
    res = aiot_dynregmq_send_request(dynregmq_handle);
    if (res < STATE_SUCCESS) {
        printf("aiot_dynregmq_send_request failed: -0x%04X\n", -res);
        return -1;
    }
			

请求发送成功后, 尝试接收服务应答

使用aiot_dynregmq_recv()接口, 以上面设置的参数, 从dynregmq服务器接收服务应答

    /* 接收动态注册请求 */
    res = aiot_dynregmq_recv(dynregmq_handle);
    if (res < STATE_SUCCESS) {
        printf("aiot_dynregmq_recv failed: -0x%04X\n", -res);
        return -1;
    }
			

以下日志对应的是白名单模式(需预录入DeviceName)下的服务应答

[1592892252.821][LK-0313] MQTT user calls aiot_mqtt_connect api, connect
establish mbedtls connection with server(host='xxx.aliyuncs.com', port=[443])
success to establish tcp, fd=6
success to establish mbedtls connection, fd = 6(cost 44809 bytes in total, max used 47721 bytes)
[1592892253.193][LK-0313] MQTT connect success in 372 ms
[1592892253.193][LK-0309] pub: /ext/register

[LK-030A] < 7B 22 64 65 76 69 63 65  53 65 63 72 65 74 22 3A | {"deviceSecret":
[LK-030A] < 22 61 31 30 64 63 36 35  35 35 64 39 32 63 64 63 | "a10dc6555d92cdc
[LK-030A] < 64 64 36 34 32 65 30 63  65 34 65 34 63 32 35 64 | dd642e0ce4e4c25d
[LK-030A] < 34 22 2C 22 70 72 6F 64  75 63 74 4B 65 79 22 3A | 4","productKey":
[LK-030A] < 22 61 31 4A 51 63 56 66  7A 73 61 6D 22 2C 22 64 | "a1JQcVfzsam","d
[LK-030A] < 65 76 69 63 65 4E 61 6D  65 22 3A 22 72 65 67 69 | eviceName":"regi
[LK-030A] < 73 74 65 72 5F 6E 6F 72  6D 61 6C 22 7D          | ster_normal"}
			

以下日志对应的是免白名单模式下的服务应答

establish mbedtls connection with server(host='xxx.aliyuncs.com', port=[443])
success to establish tcp, fd=6
success to establish mbedtls connection, fd = 6(cost 44809 bytes in total, max used 47721 bytes)
[1592892483.345][LK-0313] MQTT connect success in 503 ms
[1592892483.345][LK-0309] pub: /ext/regnwl

[LK-030A] < 7B 22 63 6C 69 65 6E 74  49 64 22 3A 22 35 6D 47 | {"clientId":"5mG
[LK-030A] < 64 6D 64 31 57 58 45 41  4C 4A 6E 33 6B 72 73 46 | dmd1WXEALJn3krsF
[LK-030A] < 30 30 30 30 31 31 30 22  2C 22 70 72 6F 64 75 63 | 0000110","produc
[LK-030A] < 74 4B 65 79 22 3A 22 61  31 4A 51 63 56 66 7A 73 | tKey":"a1JQcVfzs
[LK-030A] < 61 6D 22 2C 22 64 65 76  69 63 65 4E 61 6D 65 22 | am","deviceName"
[LK-030A] < 3A 22 72 65 67 69 73 74  65 72 5F 6E 77 6C 32 22 | :"register_nwl2"
[LK-030A] < 2C 22 64 65 76 69 63 65  54 6F 6B 65 6E 22 3A 22 | ,"deviceToken":"
[LK-030A] < 5E 31 5E 31 35 39 32 38  39 32 34 38 33 32 37 37 | ^1^1592892483277
[LK-030A] < 5E 31 31 61 66 37 33 62  37 30 35 61 63 64 62 37 | ^11af73b705acdb7
[LK-030A] < 22 7D                                            | "}
			

应答接收成功后, 在回调函数中解析应答内容

之前已经用AIOT_DYNREGMQOPT_RECV_HANDLER选项设置了, SDK收到应答时应进入的数据处理函数是demo_dynregmq_recv_handler()

void demo_dynregmq_recv_handler(void *handle, const aiot_dynregmq_recv_t *packet, void *userdata)
{
    switch (packet->type) {
        /* TODO: 回调中需要将packet指向的空间内容复制保存好, 因为回调返回后, 这些空间就会被SDK释放 */
        case AIOT_DYNREGMQRECV_DEVICEINFO_WL: {
            if (strlen(packet->data.deviceinfo_wl.device_secret) >= sizeof(demo_devinfo_wl.device_secret)) {
                break;
            }

            /* 白名单模式, 用户务必要对device_secret进行持久化保存 */
            memset(&demo_devinfo_wl, 0, sizeof(demo_devinfo_wl_t));
            memcpy(demo_devinfo_wl.device_secret, packet->data.deviceinfo_wl.device_secret,
                   strlen(packet->data.deviceinfo_wl.device_secret));
        }
        break;
        /* TODO: 回调中需要将packet指向的空间内容复制保存好, 因为回调返回后, 这些空间就会被SDK释放 */
        case AIOT_DYNREGMQRECV_DEVICEINFO_NWL: {
            if (strlen(packet->data.deviceinfo_nwl.clientid) >= sizeof(demo_devinfo_nwl.conn_clientid) ||
                strlen(packet->data.deviceinfo_nwl.username) >= sizeof(demo_devinfo_nwl.conn_username) ||
                strlen(packet->data.deviceinfo_nwl.password) >= sizeof(demo_devinfo_nwl.conn_password)) {
                break;
            }

            /* 免白名单模式, 用户务必要对MQTT的建连信息clientid, username和password进行持久化保存 */
            memset(&demo_devinfo_nwl, 0, sizeof(demo_devinfo_nwl_t));
            memcpy(demo_devinfo_nwl.conn_clientid, packet->data.deviceinfo_nwl.clientid,
                   strlen(packet->data.deviceinfo_nwl.clientid));
            memcpy(demo_devinfo_nwl.conn_username, packet->data.deviceinfo_nwl.username,
                   strlen(packet->data.deviceinfo_nwl.username));
            memcpy(demo_devinfo_nwl.conn_password, packet->data.deviceinfo_nwl.password,
                   strlen(packet->data.deviceinfo_nwl.password));
        }
        break;
        default: {

        }
        break;
    }
}
			

动态服务服务应答已经被储存在回调函数的入参packet中, 在回调中需要将它的内容复制到demo_devinfo_nwl或者demo_devinfo_wl, 因为回调返回后, SDK就要将相关的内存空间释放掉了

回到主流程, 打印服务应答的消息内容

aiot_dynregmq_recv()接口返回后(其中经过了触发并执行demo_dynregmq_recv_handler()), 服务器应答内容已被保存, 将它们打印出来

    /* 把服务应答中的信息打印出来 */
    if (nowhitelist == 0) {
        printf("device secret: %s\n", demo_devinfo_wl.device_secret);
    } else {
        printf("clientid: %s\n", demo_devinfo_nwl.conn_clientid);
        printf("username: %s\n", demo_devinfo_nwl.conn_username);
        printf("password: %s\n", demo_devinfo_nwl.conn_password);
    }
			

实际的业务使用中, 应答内容自然不只是被打印出来, 对其中的设备共享密钥deviceSecret, 一般会被保存, 然后传给SDK的MQTT接口, 用于建立MQTT长连接. 白名单模式和免白名单模式的MQTT连接配置方式不同, 白名单模式用户需要配置设备的三元组ProductKey, DeviceNameDeviceSecret; 而免白名单模式, 用户需要配置MQTT建连参数clientid, usernamepassword. 具体配置方法可以查看MQTTaiot_mqtt_setopt接口的配置选项。

销毁会话实例, 释放资源退出

动态注册的主要功能: 获取设备共享密钥(deviceSecret)已经演示完毕, 例程到这里就清理资源, 正常退出了

    /* 销毁动态注册会话实例 */
    res = aiot_dynregmq_deinit(&dynregmq_handle);