EMQX SSL双向认证配置与编码实现【JAVA】

EMQX SSL双向认证配置与mqtt.fx客户端验证【仅供参考】

一准备工作

yum install openssl
yum install vim
在opt目录下创建ssl目录用来临时存储生成的证书文件
mkdir /opt/ssl
cd /opt/ssl/
cp /etc/pki/tls/openssl.cnf ./
rm -rf /etc/pki/CA/*.old
生成证书索引库数据库文件
touch /etc/pki/CA/index.txt
指定第一个颁发证书的序列号
echo 01 > /etc/pki/CA/serial

证书生成

方法一【这个是我这边测试,服务器与安卓客户端都可用的一种】

CA证书的生成

openssl req -x509 -new -days 3650 -keyout ca.key -out rootCA.crt -nodes
参数:
Country Name (2 letter code) [XX]:国家【中国---CN】
State or Province Name (full name) []:省份
Locality Name (eg, city) [Default City]:城市
Organization Name (eg, company) [Default Company Ltd]:组织名称
Organizational Unit Name (eg, section) []:组织单元名称
Common Name (eg, your name or your server's hostname) []:服务器IP
Email Address []: 邮箱地址

为server端生成证书

一、生成私钥
openssl genrsa -out server.key 2048
二、生成证书请求csr文件
openssl req -new -key server.key -out server.csr
参数填写与前面类似
A challenge password []: 密码
An optional company name []:公司名称
三、生成证书
openssl ca -in server.csr -out server.crt -cert rootCA.crt -keyfile ca.key -days 3650

为Client端生成证书

一、生成私钥:

openssl genrsa -out client.key 2048
二、生成证书请求:
openssl req -new -key client.key -out client.csr
参数与服务端证书生成类似,不过这里我用到ip是客户端ip
三、生成证书
openssl ca -in client.csr -out client.crt -cert rootCA.crt -keyfile ca.key

修改emqx/etc/emqx.conf配置文件

将生成的CA文件复制到/opt/emqx/etc/certs目录下

listener.ssl.external.keyfile = /opt/emqx/etc/certs/server.key
listener.ssl.external.certfile = /opt/emqx/etc/certs/server.crt
listener.ssl.external.cacertfile = /opt/emqx/etc/certs/rootCA.crt

listener.ssl.external.verify = verify_peer
listener.ssl.external.fail_if_no_peer_cert = true

方法二【并非所有场景都适合】

生成CA key和证书(为了方便我这里客户端与服务器共用一个)

 生成CA key【采用2048字节】
 openssl genrsa -out ca.key 2048
 生成CA 证书【默认3650天】
 openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -subj "/CN=www.emqx.io" -out ca.pem
 

生成服务端key和证书

 openssl genrsa -out server.key 2048
 注意将IP修改为服务器IP
 openssl req -new -key ./server.key -out server.csr -subj "/CN=127.0.0.1"
 openssl x509 -req -in ./server.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out server.pem -days 3650 -sha256
 

生成客户端key和证书

openssl genrsa -out client.key 2048
注意将IP修改为客户端IP
openssl req -new -key ./client.key -out client.csr -subj "/CN=127.0.0.1"
openssl x509 -req -in ./client.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out client.pem -days 3650 -sha256

修改emqx/etc/emqx.conf配置文件

将生成的CA文件复制到/opt/emqx/etc/certs目录下

 ## SSL Options
listener.ssl.external.handshake_timeout = 15
listener.ssl.external.keyfile = /etc/certs/server.key
listener.ssl.external.certfile = /etc/certs/server.pem
## 开启双向认证
listener.ssl.external.cacertfile = /etc/certs/ca.pem
listener.ssl.external.verify = verify_peer
listener.ssl.external.fail_if_no_peer_cert = true

启用用户名密码验证【可选】

emqx是默认开启匿名认证的,即客户端不需要任何认证信息即可连上emqx服务器,但生产环境这样肯定是不行的。emqx也支持很多种认证方式,这里我选着最简单的一种,用户名密码认证

#先要把emq的匿名认证关了,在emqx.conf文件
allow_anonymous = false
**重启emqx服务**
#加载用户名认证插件
./bin/emqx_ctl plugins load emqx_auth_username
#添加用户
./bin/emqx_ctl users add <Username> <Password>

使用MQTT.fx客户端验证是否配置成功

mqtt.fx下载地址:http://www.jensd.de/apps/mqttfx/
我这里选用的是windows1.7.1的版本
EMQX SSL双向认证配置与编码实现【JAVA】_第1张图片

MQTT.fx客户端使用

创建emqx连接
EMQX SSL双向认证配置与编码实现【JAVA】_第2张图片
EMQX SSL双向认证配置与编码实现【JAVA】_第3张图片
若是启用了 用户名密码 认证,需要在这填写用户名密码信息
EMQX SSL双向认证配置与编码实现【JAVA】_第4张图片
配置客户端ssl连接证书(方法一)
EMQX SSL双向认证配置与编码实现【JAVA】_第5张图片
配置客户端ssl连接证书(方法二)
EMQX SSL双向认证配置与编码实现【JAVA】_第6张图片
连接emqx
EMQX SSL双向认证配置与编码实现【JAVA】_第7张图片
发送消息
EMQX SSL双向认证配置与编码实现【JAVA】_第8张图片

编码实现

导入MAVEN依赖

<dependency>
	<groupId>org.eclipse.paho</groupId>
	<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
	<version>1.2.0</version>
</dependency>

我这里需要将部分证书文件转换成我所需要的格式备用-【若是有其他的实现,可以不用参考我的】

cd 进入证书所在目录【方法二】
将ca.pem 与 client.pem 转化为.crt格式
openssl x509 -outform der -in your-ca.pem -out your-ca.crt
openssl x509 -outform der -in your-client.pem -out your-client.crt

将client.key转换为.pem文件【方法一、方法二通用,java代码连接需要】
openssl pkcs8 -topk8 -inform PEM -in client.key -outform PEM -nocrypt -out client-key-pkcs8.pem

将crt文件转化为p12 文件 再转换为bks文件 供安卓客户端使用
crt转p12
openssl pkcs12 -export -in client.crt -inkey client.key -out client.p12
p12 转bks
keytool -importkeystore -srckeystore client.p12 -srcstoretype pkcs12 -destkeystore client.bks -deststoretype bks -provider org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath bcprov-ext-jdk15on-157.jar 
转换过程输入的密码即为证书创建时输入的密码

若是转换出来的文件不可用,请参考:https://blog.csdn.net/qq_36992688/article/details/78861883

Java代码

MQTT静态常量类

/**
 * MQTT静态参数常量类
 */
public class MqttConstant {

    //MQTT 服务器基础配置
    /**
     * MQTT服务器IP、端口   
     */
    public final static String MQTT_IP_PORT = "ssl://127.0.0.1:8883";
    /**
     * MQTT服务器登录 用户名  elinker
     */
    public final static String MQTT_USERNAME = "username";
    /**
     * MQTT服务器登录 密码
     */
    public final static String MQTT_PASSWORD = "password";
    /**
     * MQTT客户端【应用程序】   ID【自定义】
     */
    public final static String MQTT_CLIENTID = "test";
    /**
     * MQTT客户端【应用程序】  订阅主题【自定义】
     */
    public final static String MQTT_TOPIC = "web/pubClient";

    //SSL双向认证 配置文件目录
    /**
     * CA证书
     */
    public final static String SSL_CA_CRT = "ca.crt证书文件路径";
    /**
     * 客户端证书
     */
    public final static String SSL_CLIENT_CRT = "client.crt证书文件路径";
    /**
     * 客户端证书key
     */
    public final static String SSL_CLIENT_KEY_PKCS8_PEM = "client-key-pkcs8.pem证书文件路径";
    /**
     * 客户端证书密码
     */
    public final static String SSL_CLIENT_PASSWORD = "client_password";

   //消息发送的类型
    /**
     *  尽力而为。消息发送者会想尽办法发送消息,但是遇到意外并不会重试
     */
    public final static int QOS_UNRELIABLE = 0;
    /**
     *  至少一次。消息接收者如果没有知会或者知会本身丢失,消息发送者会再次发送以保证消息接收者至少会收到一次,当然可能造成重复消息。
     */
    public final static int QOS_REPEAT = 1;
    /**
     *  恰好一次。保证这种语义肯待会减少并发或者增加延时,不过丢失或者重复消息是不可接受的时候,级别2是最合适的。
     */
    public final static int QOS_JUST = 2;


}

MQTT生产者客户端

/**
 * 服务器【应用程序】Mqtt消息推送工具类
 */
public class MqttPushUtil {

    public static Logger logger = Logger.getLogger(MqttPushUtil.class);

    public static MqttClient mqttClient;

    public static MqttConnectOptions mqttConnectOptions = new MqttConnectOptions();

    static {
        try{
            //创建客户端
            mqttClient = new MqttClient(MqttConstant.MQTT_IP_PORT, MqttConstant.MQTT_CLIENTID, new MemoryPersistence());
            //创建链接参数
            mqttConnectOptions = new MqttConnectOptions();
            //在客户端断开连接时是否缓存 订阅消息
            mqttConnectOptions.setCleanSession(true);
            //设置连接的用户名
            mqttConnectOptions.setUserName(MqttConstant.MQTT_USERNAME);
            //设置连接的密码
            mqttConnectOptions.setPassword(MqttConstant.MQTT_PASSWORD.toCharArray());
            //开启自动重连
            mqttConnectOptions.setAutomaticReconnect(true);
            //设置超时时间  单位为秒
            mqttConnectOptions.setConnectionTimeout(30);
            //设置会话心跳时间  单位为秒 服务器会每隔1.5*20秒的时间向客户端发送个消息判断客户端是否在线,但这个方法并没有重连的机制
            mqttConnectOptions.setKeepAliveInterval(20);
            //ssl 双向认证相关配置
            SSLSocketFactory  factory = EmqxSSLFactory.getSSLSocktet(MqttConstant.SSL_CA_CRT, MqttConstant.SSL_CLIENT_CRT,
                    MqttConstant.SSL_CLIENT_KEY_PKCS8_PEM, MqttConstant.SSL_CLIENT_PASSWORD);
            mqttConnectOptions.setSocketFactory(factory);
//            MqttPushServerUtil.setClient(mqttClient);


            mqttClient.connect(mqttConnectOptions);
        }catch (Exception e){
            e.printStackTrace();
            logger.error("连接MQTT服务器发生异常", e);
            try{
                mqttClient.disconnect();;
                mqttClient.close();
            }catch (MqttException el){
                e.printStackTrace();
                logger.error("连接异常时-----》断开与MQTT服务器连接操作发生异常", el);
            }
        }
    }

    public static void receiveSubscription(){
       try{
           mqttClient.setCallback(new MqttCallback() {
               /**
                *检测到断开连接
                * @param throwable
                */
               @Override
               public void connectionLost(Throwable throwable) {
                   //MQTT客户端断线重连
                   reconnection();
               }

               /**
                * 接收订阅消息处理
                * @param topic
                * @param mqttMessage
                * @throws Exception
                */
               @Override
               public void messageArrived(String topic, MqttMessage mqttMessage) throws Exception {
                   System.out.println("topic:"+topic);
                   System.out.println("Qos:"+mqttMessage.getQos());
                   System.out.println("message content:"+new String(mqttMessage.getPayload()));
               }

               /**
                * 消息发布结果
                * @param iMqttDeliveryToken
                */
               @Override
               public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
                   logger.info("MQTT消息发布的结果:---------"+ iMqttDeliveryToken.isComplete());
               }
           });

           mqttClient.subscribe(MqttConstant.MQTT_TOPIC, MqttConstant.QOS_JUST);
       }catch (Exception e){
           e.printStackTrace();
           logger.error("MQTT客户端---公租房系统:接收消息时发生异常!", e);
       }
    }

    /**
     * MQTT客户端断线重连
     */
    public static void reconnection(){
        logger.error("与MQTT服务器断开连接,尝试重新连接!");
        try {
            if(!mqttConnectOptions.isAutomaticReconnect()){
                mqttClient.reconnect();
            }
        }catch (MqttException e){
            logger.error("MQTT重新连接发生异常!", e);
        }
    }

    /**
     * 发布非可靠的消息【消息服务质量:0】
     * @param topic   发布主题
     * @param pushMessage  消息内容
     */
    public static boolean publishUnreliable(String topic, String pushMessage) {
        return publish(MqttConstant.QOS_UNRELIABLE, topic, pushMessage);
    }

    /**
     * 以至少收到一次的模式发送消息【可能重复,消息服务质量:1】
     * @param topic  发布主题
     * @param pushMessage  消息内容
     */
    public static boolean publishLeastOnce(String topic, String pushMessage) {
        return publish(MqttConstant.QOS_REPEAT, topic, pushMessage);
    }

    /**
     * 发送可靠的保证 能且只能收到一次的消息【消息服务质量:2】
     * @param topic
     * @param pushMessage
     */
    public static boolean publishReliable(String topic, String pushMessage) {
        return publish(MqttConstant.QOS_JUST, topic, pushMessage);
    }

    /**
     * 发布主题和消息队列
     * @param qos
     * @param topic
     * @param pushMessage
     * @return
     */
    public static boolean publish(int qos, String topic, String pushMessage) {
        try {
            // 创建消息
            MqttMessage message = new MqttMessage(pushMessage.getBytes());
            // 设置消息的服务质量
            message.setQos(qos);
            // 发布消息
            mqttClient.publish(topic, message);

        } catch (MqttException e) {
            logger.error("发布消息时发生异常!",e);
            return false;
        }
        return true;
    }

}

EmqxSSL连接工厂类

public class EmqxSSLFactory {

    public static javax.net.ssl.SSLSocketFactory getSSLSocktet(String caPath, String crtPath, String keyPath, String password) {
        try{
            CertificateFactory cAf = CertificateFactory.getInstance("X.509");
            FileInputStream caIn = new FileInputStream(caPath);
            X509Certificate ca = (X509Certificate) cAf.generateCertificate(caIn);
            KeyStore caKs = KeyStore.getInstance("JKS");
            caKs.load(null, null);
            caKs.setCertificateEntry("ca-certificate", ca);
            TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");
            tmf.init(caKs);

            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            FileInputStream crtIn = new FileInputStream(crtPath);
            X509Certificate caCert = (X509Certificate) cf.generateCertificate(crtIn);

            crtIn.close();
            KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
            ks.load(null, null);
            ks.setCertificateEntry("certificate", caCert);
            ks.setKeyEntry("private-key", getPrivateKey(keyPath), password.toCharArray(),
                    new java.security.cert.Certificate[]{caCert});
            KeyManagerFactory kmf = KeyManagerFactory.getInstance("PKIX");
            kmf.init(ks, password.toCharArray());

            SSLContext context = SSLContext.getInstance("TLSv1");

            context.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());
            return context.getSocketFactory();
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

    public static PrivateKey getPrivateKey(String path) throws Exception {

        org.apache.commons.codec.binary.Base64 base64 = new Base64();
        byte[] buffer = base64.decode(getPem(path));

        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(buffer);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        return (RSAPrivateKey) keyFactory.generatePrivate(keySpec);

    }

    private static String getPem(String path) throws Exception {
        FileInputStream fin = new FileInputStream(path);
        BufferedReader br = new BufferedReader(new InputStreamReader(fin));
        String readLine = null;
        StringBuilder sb = new StringBuilder();
        while ((readLine = br.readLine()) != null) {
            if (readLine.charAt(0) == '-') {
                continue;
            } else {
                sb.append(readLine);
                sb.append('\r');
            }
        }
        fin.close();
        return sb.toString();
    }
}

测试类

public class MqttPushClientUtil {

    public static void main(String[] args) throws MqttException {
        String HOST = MqttConstant.MQTT_IP_PORT;
        String[] TOPIC = {"dev/mac/+", "dev/village/+"};
        int[] qos = new int[]{2, 2};
        String clientid = "subClient";
        String userName = "username";
        String passWord = "password";
        try {
            // host为主机名,test为clientid即连接MQTT的客户端ID,一般以客户端唯一标识符表示,MemoryPersistence设置clientid的保存形式,默认为以内存保存
            MqttClient client = new MqttClient(HOST, clientid, new MemoryPersistence());
            // MQTT的连接设置
            MqttConnectOptions options = new MqttConnectOptions();
            // 设置是否清空session,这里如果设置为false表示服务器会保留客户端的连接记录,这里设置为true表示每次连接到服务器都以新的身份连接
            SSLSocketFactory  factory = EmqxSSLFactory.getSSLSocktet(MqttConstant.SSL_CA_CRT, MqttConstant.SSL_CLIENT_CRT,
                    MqttConstant.SSL_CLIENT_KEY_PKCS8_PEM, MqttConstant.SSL_CLIENT_PASSWORD);
            options.setSocketFactory(factory);
            options.setCleanSession(true);
            // 设置连接的用户名
            options.setUserName(userName);
            // 设置连接的密码
            options.setPassword(passWord.toCharArray());
            // 设置超时时间 单位为秒
            options.setConnectionTimeout(10);
            // 设置会话心跳时间 单位为秒 服务器会每隔1.5*20秒的时间向客户端发送个消息判断客户端是否在线,但这个方法并没有重连的机制
            options.setKeepAliveInterval(20);
            // 设置回调函数
            client.setCallback(new MqttCallback() {

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

                @Override
                public void messageArrived(String topic, MqttMessage message) throws Exception {
                    System.out.println("topic:"+topic);
                    System.out.println("Qos:"+message.getQos());
                    System.out.println("message content:"+new String(message.getPayload()));

                }

                @Override
                public void deliveryComplete(IMqttDeliveryToken token) {
                    System.out.println("deliveryComplete---------"+ token.isComplete());
                }

            });
            client.connect(options);
            //订阅消息
            client.subscribe(TOPIC, qos);
        } catch (Exception e) {
            e.printStackTrace();
        }


        Map<String, Object> map = new HashMap<>();
        map.put("index", 1);
        map.put("text", "设备信息");
        MqqtPushServiceUtil.pushDeviceMsg("00:00:a4:01:01:01", "12", map);
        try{
            Thread.sleep(5000);
        }catch (Exception e){
            e.printStackTrace();
        }

        map.put("index", 2);
        map.put("text", "小区信息");
        MqqtPushServiceUtil.pushVillageMsg(123, "12", map);
    }

}

参考

证书格式转换: https://vimsky.com/article/3608.html
其他证书生成方式:
emqtt安全连接ssl配置 自签证书 单向认证连接 加密 ssl/tls,mosquitto客户端ssl单向认证连接测试
emqx使用自制CA证书登录配置(双向认证)
EMQ X 服务器 SSL/TLS 安全连接配置指南
MQTT研究之EMQ:【SSL双向验证】
MQTT研究之EMQ:【JAVA代码构建X509证书】
其他编码实现
MQTT Java客户端Eclipse paho实现数据的发送和接收
emqtt 试用(八)ssl认证 - 代码验证
MQTT.fx客户端安装与使用
MQTT.fx的安装和使用
EMQX权限验证
emqx服务器的权限验证(四)

你可能感兴趣的:(emqx,java)