本文以一型一密預注冊的Java代碼為例,介紹基于MQTT通信協議的設備,如何進行動態注冊并獲取接入物聯網平臺認證需要的DeviceSecret。

前提條件

已完成一型一密文檔中的以下步驟:

  1. 創建產品。
  2. 開啟動態注冊。
  3. 添加設備。
  4. 產線燒錄。

背景信息

物聯網平臺支持多種設備安全認證方式,具體認證方式,請參見設備安全認證

物聯網平臺支持基于MQTT通道的一型一密預注冊和免預注冊認證。相關流程和參數說明,請參見基于MQTT通道的設備動態注冊

準備開發環境

本示例使用的開發環境如下:

操作步驟

  1. 打開IntelliJ IDEA,創建一個Maven工程。例如MQTT動態注冊
  2. 在工程中的pom.xml文件中,添加Maven依賴,然后單擊Load Maven Changes圖標,完成依賴包下載。
    <dependency>
      <groupId>org.eclipse.paho</groupId>
      <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
      <version>1.2.1</version>
    </dependency>
    
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>fastjson</artifactId>
      <version>1.2.83</version>
    </dependency>
  3. 在工程MQTT動態注冊的路徑/src/main/java下,創建Java類。例如DynamicRegisterByMqtt,輸入以下代碼。
    說明
    • 設備未激活時,可進行多次動態注冊,設備的DeviceSecret以最后一次為準。請確保固化到設備的DeviceSecret為最新。
    • 設備已激活時,您需調用ResetThing接口重置云端設備動態注冊狀態為未注冊,才能再次動態注冊該設備。
    import java.nio.charset.StandardCharsets;
    import java.util.Random;
    import java.util.Set;
    import java.util.SortedMap;
    import java.util.TreeMap;
    
    import javax.crypto.Mac;
    import javax.crypto.spec.SecretKeySpec;
    
    import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
    import org.eclipse.paho.client.mqttv3.MqttCallback;
    import org.eclipse.paho.client.mqttv3.MqttClient;
    import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
    import org.eclipse.paho.client.mqttv3.MqttException;
    import org.eclipse.paho.client.mqttv3.MqttMessage;
    import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
    
    import com.alibaba.fastjson.JSONObject;
    
    /**
     * 設備動態注冊。 
     */
    public class DynamicRegisterByMqtt {
    
        // 地域ID,填寫您的產品所在地域ID。
        private static String regionId = "cn-shanghai";
    
        // 定義加密方式。可選MAC算法:HmacMD5、HmacSHA1、HmacSHA256,需和signmethod取值一致。
        private static final String HMAC_ALGORITHM = "hmacsha1";
    
        // 接收物聯網平臺下發設備證書的Topic。無需創建,無需訂閱,直接使用。
        private static final String REGISTER_TOPIC = "/ext/register";
    
        /**
         * 動態注冊。
         * 
         * @param productKey 產品的ProductKey
         * @param productSecret 產品密鑰
         * @param deviceName 設備名稱
         * @throws Exception
         */
        public void register(String productKey, String productSecret, String deviceName) throws Exception {
    
            // 接入域名,只能使用TLS。
            String broker = "ssl://" + productKey + ".iot-as-mqtt." + regionId + ".aliyuncs.com:1883";
    
            // 表示客戶端ID,建議使用設備的MAC地址或SN碼,64字符內。
            String clientId = productKey + "." + deviceName;
    
            // 獲取隨機值。
            Random r = new Random();
            int random = r.nextInt(1000000);
    
            // securemode只能為2表示只能使用TLS;signmethod指定簽名算法。
            String clientOpts = "|securemode=2,authType=register,signmethod=" + HMAC_ALGORITHM + ",random=" + random + "|";
    
            // MQTT接入客戶端ID。
            String mqttClientId = clientId + clientOpts;
    
            // MQTT接入用戶名。
            String mqttUsername = deviceName + "&" + productKey;
    
            // MQTT接入密碼,即簽名。
            JSONObject params = new JSONObject();
            params.put("productKey", productKey);
            params.put("deviceName", deviceName);
            params.put("random", random);
            String mqttPassword = sign(params, productSecret);
    
            // 通過MQTT connect報文進行動態注冊。
            connect(broker, mqttClientId, mqttUsername, mqttPassword);
        }
    
        /**
         * 通過MQTT connect報文發送動態注冊信息。
         * 
         * @param serverURL 動態注冊域名地址
         * @param clientId 客戶端ID
         * @param username MQTT用戶名
         * @param password MQTT密碼
         */
        @SuppressWarnings("resource")
        private void connect(String serverURL, String clientId, String username, String password) {
            try {
                MemoryPersistence persistence = new MemoryPersistence();
                MqttClient sampleClient = new MqttClient(serverURL, clientId, persistence);
                MqttConnectOptions connOpts = new MqttConnectOptions();
                connOpts.setMqttVersion(4);// MQTT 3.1.1
                connOpts.setUserName(username);// 用戶名
                connOpts.setPassword(password.toCharArray());// 密碼
                connOpts.setAutomaticReconnect(false); // MQTT動態注冊協議規定必須關閉自動重連。
                System.out.println("----- register params -----");
                System.out.print("server=" + serverURL + ",clientId=" + clientId);
                System.out.println(",username=" + username + ",password=" + password);
                sampleClient.setCallback(new MqttCallback() {
                    @Override
                    public void messageArrived(String topic, MqttMessage message) throws Exception {
                        // 僅處理動態注冊返回消息。
                        if (REGISTER_TOPIC.equals(topic)) {
                            String payload = new String(message.getPayload(), StandardCharsets.UTF_8);
                            System.out.println("----- register result -----");
                            System.out.println(payload);
                            sampleClient.disconnect();
                        }
                    }
    
                    @Override
                    public void deliveryComplete(IMqttDeliveryToken token) {
                    }
    
                    @Override
                    public void connectionLost(Throwable cause) {
                    }
                });
                sampleClient.connect(connOpts);
            } catch (MqttException e) {
                System.out.print("register failed: clientId=" + clientId);
                System.out.println(",username=" + username + ",password=" + password);
                System.out.println("reason " + e.getReasonCode());
                System.out.println("msg " + e.getMessage());
                System.out.println("loc " + e.getLocalizedMessage());
                System.out.println("cause " + e.getCause());
                System.out.println("except " + e);
                e.printStackTrace();
            }
        }
    
        /**
         * 動態注冊簽名。
         * 
         * @param params 簽名參數
         * @param productSecret 產品密鑰
         * @return 簽名十六進制字符串
         */
        private String sign(JSONObject params, String productSecret) {
    
            // 請求參數按字典順序排序。
            Set<String> keys = getSortedKeys(params);
    
            // sign、signMethod除外。
            keys.remove("sign");
            keys.remove("signMethod");
    
            // 組裝簽名明文。
            StringBuffer content = new StringBuffer();
            for (String key : keys) {
                content.append(key);
                content.append(params.getString(key));
            }
    
            // 計算簽名。
            String sign = encrypt(content.toString(), productSecret);
            System.out.println("sign content=" + content);
            System.out.println("sign result=" + sign);
    
            return sign;
        }
    
        /**
         * 獲取JSON對象排序后的key集合。
         *
         * @param json 需要排序的JSON對象
         * @return 排序后的key集合
         */
        private Set<String> getSortedKeys(JSONObject json) {
            SortedMap<String, String> map = new TreeMap<String, String>();
            for (String key : json.keySet()) {
                String value = json.getString(key);
                map.put(key, value);
            }
            return map.keySet();
        }
    
        /**
         * 使用HMAC_ALGORITHM加密。
         * 
         * @param content 明文
         * @param secret 密鑰
         * @return 密文
         */
        private String encrypt(String content, String secret) {
            try {
                byte[] text = content.getBytes(StandardCharsets.UTF_8);
                byte[] key = secret.getBytes(StandardCharsets.UTF_8);
                SecretKeySpec secretKey = new SecretKeySpec(key, HMAC_ALGORITHM);
                Mac mac = Mac.getInstance(secretKey.getAlgorithm());
                mac.init(secretKey);
                return byte2hex(mac.doFinal(text));
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    
        /**
         * 二進制轉十六進制字符串。
         * 
         * @param b 二進制數組
         * @return 十六進制字符串
         */
        private String byte2hex(byte[] b) {
            StringBuffer sb = new StringBuffer();
            for (int n = 0; b != null && n < b.length; n++) {
                String stmp = Integer.toHexString(b[n] & 0XFF);
                if (stmp.length() == 1) {
                    sb.append('0');
                }
                sb.append(stmp);
            }
            return sb.toString().toUpperCase();
        }
    
        public static void main(String[] args) throws Exception {
    
            String productKey = "a1IoK******";
            String productSecret = "6vEu5Qlj5S******";
            String deviceName = "OvenDevice01";
    
            // 進行動態注冊。
            DynamicRegisterByMqtt client = new DynamicRegisterByMqtt();
            client.register(productKey, productSecret, deviceName);
    
            // 動態注冊成功,需要在本地固化deviceSecret。
        }
    }
  4. 在以上代碼中配置實際設備相關參數。
    參數 示例 說明
    regionId cn-shanghai 您的物聯網平臺服務所在地域ID。地域代碼表達方法,請參見地域列表
    productKey a1IoK****** 已燒錄至設備的產品ProductKey,可登錄物聯網平臺控制臺,在產品詳情頁查看。
    productSecret 6vEu5Qlj5S****** 已燒錄至設備的產品ProductSecret,可登錄物聯網平臺控制臺,在產品詳情頁查看。
    deviceName OvenDevice01 您設備的名稱。

    因設備激活時會校驗DeviceName,建議您采用可以直接從設備中讀取到的ID,如設備的MAC地址、IMEI或SN碼等,作為DeviceName使用。

    broker "ssl://" + productKey + ".iot-as-mqtt." + regionId + ".aliyuncs.com:1883" 設備動態注冊的接入點。格式為如下ssl://" + "${YourInstanceDomain}" + ":" +1883

    其中${YourInstanceDomain}為MQTT接入域名。獲取方法,請參見查看實例終端節點

  5. 運行程序文件DynamicRegisterByMqtt.java,使設備攜帶DeviceName和所屬產品的ProductKey、ProductSecret向云端發起認證請求。

    執行結果如圖所示,物聯網平臺校驗通過后,設備接收到云端下發的DeviceSecret(8d1f0cdab49dd229cf3b75**********)。

    代碼運行結果

后續步驟

設備獲得連接云端所需的設備證書(ProductKey、DeviceName和DeviceSecret)后,您再使用MQTT客戶端,將設備接入物聯網平臺,進行數據通信。

具體操作,請參見Paho-MQTT Java接入示例