快速入门:使用 JDK 实现基于证书的双向认证


该文章帮助你快速入门,使用 JDK 实现设备与 EnOS Cloud 之间的基于证书的双向安全连接。

开始前准备:创建模型、产品、设备

本步骤的前提是你已完成 直连设备连接快速入门子设备通过edge连接至EnOS Cloud快速入门这两个示例。

创建 EnOS Edge 产品

本步骤大部分与上述两个快速入门示例相似,差异点在创建 EnOS Edge 产品时需要创建启用 证书双向认证机制 的产品,默认证书有效期和最长证书有效期使用默认值即可。如下图所示:

../_images/edge_ssl.png

逆变器产品不需要开启 证书双向认证机制,因为逆变器是作为子设备由 EnOS Edge 代理连接 EnOS Cloud,只需要 edge 与 cloud 进行基于证书的双向认证即可。

创建 EnOS Edge 设备

基于以上产品创建网关类型设备 Edge01_Certificate。如下图所示:

../_images/edge01_certificate.png


记下 Edge01_Certificate 的设备三元组,将用于创建证书请求文件。以下设备三元组供您参考,您需要使用使用自己的三元组。

  • Product Key:Et***YP6
  • Device Key:UB***rOhJD
  • Device Secret:jgWGPE***B7bShf2P5cz

创建子设备

逆变器设备参照 直连设备连接快速入门进行创建。如下图所示:

../_images/INV002.png

步骤 1:使用 JDK 生成连接 EnOS Cloud 需要的 JKS 文件

以下代码示例执行的功能是:

  1. 生成证书签名请求 (Certificate Signing Request, CSR) 文件
  2. 调用 EnOS API 申请证书
  3. 生成 EnOS Edge 设备连接平台需要的 JKS 文件
import com.envision.apim.poseidon.config.PConfig;
import com.envision.apim.poseidon.core.Poseidon;
import com.envisioniot.enos.connect_service.v2_1.cert.ApplyCertificateRequest;
import com.envisioniot.enos.connect_service.v2_1.cert.ApplyCertificateResponse;
import com.envisioniot.enos.connect_service.vo.DeviceIdentifier;
import sun.security.pkcs10.PKCS10;
import sun.security.x509.X500Name;

import java.io.*;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;

public class sampleCode {
    /**
     * 证书密钥生成配置
     */
    // ALGORITHM表示证书算法,可选的值有:RSA or EC,分别表示RSA算法和ECC算法
    public static final String ALGORITHM = "EC";
    // ISSUE_AUTHORITY表示证书类型,可选值有: RSA 和 ECC
    public static final String ISSUE_AUTHORITY = "ECC";
    // VALID_DAY表示证书有效期
    public static final Integer VALID_DAY = 300;
    //IS_ECC_CONNECT布尔值,表示是否使用ECC证书连接EnOS。如果使用RSA证书,其值应为false
    public static boolean IS_ECC_CONNECT = true;
    /**
     * 设备认证所需信息,这些参数依次分别表示EnOS Edge设备的device key, product key, device secret和asset ID
     * 在代码中,需要使用 DEVICE_KEY + PRODUCT_KEY或单独使用 ASSET_ID来指定一个设备
     * 因此,可以选择使用任意一种方法,另外一种方法所需要的参数无需保留在代码中
     */
    public static final String DEVICE_KEY = "yourDeviceKey";
    public static final String PRODUCT_KEY = "yourProductKey";
    public static final String DEVICE_SECRET = "yourDeviceSecret";
    public static final String ASSET_ID = "yourAssetID";


    /**
     * 如果使用RSA算法,SIZE 值为2048,表示密钥长度为2048比特
     * 如果使用ECC算法,SIZE 值为256,表示使用prime256v1算法
     */
    public static final Integer SIZE = 256;
    /**
     * SIGNER_ALGORITHM表示签名算法
     * 如果使用RSA证书,则其值为SHA256withRSA
     * 如果使用ECC证书,则其值为SHA256WITHECDSA
     */
    public static final String SIGNER_ALGORITHM = "SHA256WITHECDSA";
    /**
     * 请求主体信息
     * COMMON_NAME 常用名 不大于64个字符
     * ORGANIZATION_UNIT 组织下的单位或部门名称 不大于64个字符
     * ORGANIZATION 组织名称 不大于64个字符
     * LOCALE 城市名称 不大于64个字符
     * STATE_OR_PROVINCE_NAME 州或省份名称 不大于64个字符
     * COUNTRY 国家名称 必须为2个字符,详情参见 countrycode.org
     */
    public static final String COMMON_NAME = "doc ecc cert test";
    public static final String ORGANIZATION_UNIT = "Envision cloud";
    public static final String ORGANIZATION = "Envision";
    public static final String LOCALE = "Shanghai";
    public static final String STATE_OR_PROVINCE_NAME = "Shanghai";
    public static final String COUNTRY = "CN";
    /**
     * 在EnOS上注册的,用于调用API的应用的Access key和secret key,可以在 “EnOS控制台 > 应用注册” 获取
     */
    public static final String ACCESS_KEY = "yourAccessKey";
    public static final String SECRET_KEY = "yourSecretKey";
    // API网关URL,点击控制台右上角的 帮助 > 环境信息 查询
    public static final String API_GATEWAY_URL = "http://apim-apigw-proxy";
    /**
     * 组织ID,鼠标悬停在EnOS控制台左上角的OU名称处可以获得
     */
    public static final String ORG_ID = "yourOrgID";

    //用于保存生成的公私钥对
    private static PrivateKey PRIVATE_KEY;

    /**
     * 以下参数用于保存生成的各种文件:
     * SAVE_CSR_FILE_PATH 用于保存生成的CSR文件
     * SAVE_DEVICE_CERT_FILE_PATH 用于保存申请到的设备证书
     * SAVE_ROOT_CERT_FILE_PATH 用于保存根证书
     * JKS_PASSWORD 用于保存JKS密码
     */
    public static final String SAVE_CSR_FILE_PATH = "edge.csr";
    public static final String SAVE_DEVICE_CERT_FILE_PATH = "edge.pem";
    public static final String SAVE_ROOT_CERT_FILE_PATH = "cacert.pem";
    public static final String JKS_PASSWORD = "123456";

    public static final String PRIVATE_ENTRY_NAME = "edge";
    public static final String CA_CERT_ENTRY_NAME = "cacert";
    public static final String JKS_FILE_NAME = "edge.jks";


    //生成证书签名请求文件公私钥对
    public static KeyPair generateKeyPair(String algorithm, int size) throws NoSuchAlgorithmException {
        KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(algorithm);
        keyPairGen.initialize(size, new SecureRandom());
        return keyPairGen.generateKeyPair();
    }

    //生成CSR文件
    public static void createCsrFile() throws CertificateException, SignatureException, NoSuchAlgorithmException, InvalidKeyException, IOException {
        try (ByteArrayOutputStream bs = new ByteArrayOutputStream();
             PrintStream ps = new PrintStream(bs)) {
            KeyPair keyPair = generateKeyPair(ALGORITHM, SIZE);
            PKCS10 pkcs10 = new PKCS10(keyPair.getPublic());
            Signature signature = Signature.getInstance(SIGNER_ALGORITHM);
            signature.initSign(keyPair.getPrivate());
            // 保存私钥,用于生成JKS文件
            PRIVATE_KEY = keyPair.getPrivate();
            X500Name x500Name = new X500Name(COMMON_NAME, ORGANIZATION_UNIT, ORGANIZATION, LOCALE, STATE_OR_PROVINCE_NAME, COUNTRY);
            pkcs10.encodeAndSign(x500Name, signature);
            pkcs10.print(ps);
            byte[] c = bs.toByteArray();
            String csrFileString = new String(c);
            // 保存CSR文件至指定路径
            saveFile(csrFileString, SAVE_CSR_FILE_PATH);
        }
    }

    /**
     * 保存文件
     */
    public static void saveFile(String fileContent, String filePath) throws IOException {
        File fp = new File(filePath);
        try (OutputStream os = new FileOutputStream(fp)) {
            os.write(fileContent.getBytes());
        }
    }


    public static void main(String[] args) throws NoSuchAlgorithmException, CertificateException, SignatureException, InvalidKeyException, IOException, KeyStoreException {
        // step1: 请确定ISSUE_AUTHORITY和你生成csr对应的颁发方是一致的文件
        createCsrFile();
        // step2: 调用EnOS API获取证书
        applyCertToDevice();
        // step3: 利用得到的书和根证书生成jks
        createDeviceJks();
    }


    /**
    *用于生成JKS文件
    *
    */
    public static void createDeviceJks() throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException {
        // step1: 读取设备证书
        String deviceCertificate = readFile(SAVE_DEVICE_CERT_FILE_PATH);
        // ==> 转换成X.509证书
        X509Certificate x509Certificate = parseCertificate(deviceCertificate);
        // step2: 读取根证书
        String rootCaCertificate = readFile(SAVE_ROOT_CERT_FILE_PATH);
        X509Certificate rootCertificate = parseCertificate(rootCaCertificate);
        // step3: 创建密钥库
        KeyStore keyStore = KeyStore.getInstance("jks");
        // 初始化
        keyStore.load(null, null);
        // step4: 将证书与私钥保存到密钥库
        addPrivateEntry(keyStore, JKS_PASSWORD, PRIVATE_KEY, PRIVATE_ENTRY_NAME, x509Certificate, rootCertificate);
        // step5: 将根证书加入JKS授信列表
        addTrustEntry(keyStore, CA_CERT_ENTRY_NAME, rootCertificate);
        // step6: 保存JKS文件
        saveKeystore(keyStore, JKS_FILE_NAME, JKS_PASSWORD);
    }

    //将证书与私钥保存到密钥库
    public static void addPrivateEntry(KeyStore caKs, String password, PrivateKey privateKey, String privateEntryName, X509Certificate... chainCerts) {
        try {
            if (caKs != null && chainCerts != null && privateKey != null &&
                    privateEntryName != null && !privateEntryName.isEmpty()) {
                assert chainCerts.length > 0 : "chainCert don't input!";
                KeyStore.PrivateKeyEntry skEntry = new KeyStore.PrivateKeyEntry(privateKey, chainCerts);
                caKs.setEntry(privateEntryName, skEntry, new KeyStore.PasswordProtection(password.toCharArray()));
            }
        } catch (KeyStoreException e) {
            System.err.println("when add private entry is occur error!");
        }
    }

    //将根证书加入JKS授信列表
    public static void addTrustEntry(KeyStore caKs, String entryName, X509Certificate rootCert) {
        try {
            if (rootCert != null && caKs != null && entryName != null && !entryName.isEmpty()) {
                caKs.setEntry(entryName, new KeyStore.TrustedCertificateEntry(rootCert), null);
            }
        } catch (KeyStoreException e) {
            System.err.println("when add trust entry is occur error!");
        }
    }

    //保存JKS文件
    public static void saveKeystore(KeyStore keyStore, String jksFileName, String password)
            throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException {
        // 保存成jks文件
        try (FileOutputStream fos = new FileOutputStream(jksFileName)) {
            keyStore.store(fos, password.toCharArray());
        }
    }

    //转换成X.509证书
    public static X509Certificate parseCertificate(String certificateContentString) throws IOException {
        if (certificateContentString != null && !certificateContentString.trim().isEmpty()) {
            try (InputStream inputStream = new ByteArrayInputStream(certificateContentString.getBytes())) {
                CertificateFactory cf = CertificateFactory.getInstance("X.509");
                return (X509Certificate) cf.generateCertificate(inputStream);
            } catch (CertificateException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    //将证书绑定至设备
    public static void applyCertToDevice() throws IOException {
        // 读取生成的csr文件
        String certificateRequest = readFile(SAVE_CSR_FILE_PATH);
        // 设置绑定证书设备的请求参数
        ApplyCertificateRequest applyCertificateRequest = createApplyCertParam(certificateRequest);
        // 利用EnOS API发起请求
        ApplyCertificateResponse certRsp =
                Poseidon.config(PConfig.init().appKey(ACCESS_KEY).appSecret(SECRET_KEY).debug())
                        .url(API_GATEWAY_URL)
                        .getResponse(applyCertificateRequest, ApplyCertificateResponse.class);
        //获得设备的证书和根证书
        if (certRsp.success()) {
            // 保存设备的证书
            saveFile(certRsp.getData().getCert(), SAVE_DEVICE_CERT_FILE_PATH);
            // 保存设备对应的根证书
            saveFile(certRsp.getData().getCaCert(), SAVE_ROOT_CERT_FILE_PATH);
        }
    }

    //设置绑定证书至设备的请求参数
    public static ApplyCertificateRequest createApplyCertParam(String certificateRequest) {
        ApplyCertificateRequest applyCertificateRequest = new ApplyCertificateRequest();
        applyCertificateRequest.setCsr(certificateRequest);
        /*
         * 请确定ISSUE_AUTHORITY和你生成csr对应的颁发方是一致的
         */
        applyCertificateRequest.setIssueAuthority(ISSUE_AUTHORITY);
        /*
         * 请确保证书的有效天数满足小于所属产品规定的最大有效期
         */
        applyCertificateRequest.setValidDay(VALID_DAY);
        /*
         * 设备信息
         */
        DeviceIdentifier deviceIdentifier = new DeviceIdentifier();
        /*
         * 使用以下任意一个参数或参数组合以指定设备:
         * ASSET_ID
         * PRODUCT_KEY + DEVICE_KEY
         */
        applyCertificateRequest.setAssetId(ASSET_ID);
        applyCertificateRequest.setProductKey(PRODUCT_KEY);
        applyCertificateRequest.setDeviceKey(DEVICE_KEY);
        /*
         * 组织ID
         */
        applyCertificateRequest.setOrgId(ORG_ID);
        return applyCertificateRequest;
    }

    /**
     * 读取文件
     */
    public static String readFile(String path) {
        try {
            return Files.lines(Paths.get(path), StandardCharsets.UTF_8)
                    .collect(Collectors.joining("\n"));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return EMPTY_STRING;
    }
}

步骤 2:使用 SDK 连接 EnOS Edge 设备至 EnOS


你可使用 MQTT SDK 或 HTTP SDK 连接。

MQTT SDK

在 Java 项目的 pom.xml 文件中添加 MQTT SDK 的依赖,版本最低必须为 2.2.5,如下所示:

<dependency>
    <groupId>com.envisioniot</groupId>
    <artifactId>enos-mqtt</artifactId>
    <version>2.2.16</version>
</dependency>


使用 MQTT SDK 连接 EnOS 的示例代码段如下所示,将这段代码替换 快速入门:将非智能设备通过 edge 连接至 EnOS Cloud 步骤 5 中的 “网关上线” 示例代码:

public static boolean IS_ECC_CONNECT = true; //使用RSA证书时,该参数值为false;使用ECC证书时,值为true
public static final String DEVICE_KEY = "yourDeviceKey";
public static final String PRODUCT_KEY = "yourProductKey";
public static final String DEVICE_SECRET = "yourDeviceSecret";
public static final String JKS_PASSWORD = "yourJksPassword";
public static final String JKS_FILE_NAME = "edge.jks";
public static final String SSL_CONNECT_URL = "ssl://MqttBrokerUrl:18883"; // MQTT broker URL,点击控制台右上角的 帮助 > 环境信息 查询


private static void connectEnos() {
    DefaultProfile defaultProfile = new DefaultProfile(SSL_CONNECT_URL, PRODUCT_KEY, DEVICE_KEY,DEVICE_SECRET);
    // 设置连接属性
    defaultProfile.setConnectionTimeout(60).setKeepAlive(180).setAutoReconnect(false)
            .setSSLSecured(true)
            // 设置双向认证,设备的jks文件路径和读取密码
            .setSSLJksPath(JKS_FILE_NAME, JKS_PASSWORD)
            // 设置是否是Ecc证书连接平台
            .setEccConnect(IS_ECC_CONNECT);
    final MqttClient mqttClient = new MqttClient(defaultProfile);
    mqttClient.connect(new ConnCallback() {
        @Override
        public void connectComplete(boolean reconnect) {
            System.out.println("connect success");
        }

        @Override
        public void connectLost(Throwable cause) {
            System.out.println("connect lost");
        }

        @Override
        public void connectFailed(Throwable cause) {
            System.out.println("onConnectFailed : " + cause);
        }
    });
}

HTTP SDK

在 Java 项目的 pom.xml 文件中添加 HTTP-SDK 的依赖,版本最低必须为 0.1.9,如下所示:

<dependency>
    <groupId>com.envisioniot</groupId>
    <artifactId>enos-http</artifactId>
    <version>0.2.1</version>
</dependency>


使用 HTTP SDK 连接 EnOS 的示例代码段如下所示,将这段代码替换 快速入门:将非智能设备通过 edge 连接至 EnOS Cloud 步骤 5 中的 “网关上线” 示例代码:

public class HttpBiDirectionalAuthenticate {
    // EnOS HTTP Broker URL, which can be obtained from Environment Information page in EnOS Console
    // ssl port 8443
    static final String BROKER_URL = "https://broker_url:8443/";

    // Device credentials, which can be obtained from Device Details page in EnOS Console
    static final String PRODUCT_KEY = "productKey";
    static final String DEVICE_KEY = "deviceKey";
    static final String DEVICE_SECRET = "deviceSecret";

    private static String jksPath = "jskPath";
    private static String jksPassword = "jskPassword";

    /** Ecc cert flag
     * if use ECC certificate, chose true
     * if use RSA certificate, chose false */
    static final boolean IS_ECC_CONNECT = false;

    public static void main(String[] args) throws EnvisionException {
        // construct a static device credential via ProductKey, DeviceKey and DeviceSecret
        StaticDeviceCredential credential = new StaticDeviceCredential(
                PRODUCT_KEY, DEVICE_KEY, DEVICE_SECRET);

        // construct a http connection
        SessionConfiguration configuration = SessionConfiguration
                .builder()
                .lifetime(30_000)
                .sslSecured(true)
                .isEccConnect(IS_ECC_CONNECT)
                .jksPath(jksPath)
                .jksPassword(jksPassword)
                .build();

        HttpConnection connection = new HttpConnection.Builder(BROKER_URL, credential)
                .sessionConfiguration(configuration)
                .build();

        MeasurepointPostRequest request = buildMeasurepointPostRequest();

        try
        {
            MeasurepointPostResponse response = connection.publish(request, null);
            System.out.println(new GsonBuilder().setPrettyPrinting().create().toJson(response));
        } catch (EnvisionException | IOException e)
        {
            e.printStackTrace();
        }
    }

    private static MeasurepointPostRequest buildMeasurepointPostRequest()
    {
        // Measurepoints are defined in ThingModel
        return MeasurepointPostRequest.builder()
                .addMeasurePoint("Int_value", 100)
                .addMeasurePoint("DI_value_01", 5)
                .build();
    }
}

步骤 3:启动示例程序

完成步骤5的替换操作后,运行快速入门:将非智能设备通过 edge 连接至 EnOS Cloud 中更新后的示例代码。

步骤 4:检查设备连接状态

在运行示例程序以后,EnOS Edge 上线,并添加子设备作为拓扑,代理子设备连接云端。设备连接状态如下图所示:

../_images/device_list.png

步骤 5:查看设备数据

进入控制台,选择 设备管理 > 设备资产,点击 view 查看 INV001设备详情,打开 测点 标签页,选择 INV.GenActivePW 测点,点击 查看数据,可以查看历史数据记录。