mosquitto的TLS功能测试,客户端使用paho.mqtt.golang(附JAVA版客户端实现)
1、SSL/TLS简介
SSL(SecureSocket Layer)安全套接层,是网景公司提出的用于保证Server与client之间安全通信的一种协议,该协议位于TCP/IP协议与各应用层协议之间,即SSL独立于各应用层协议,因此各应用层协议可以透明地调用SSL来保证自身传输的安全性。目前,SSL被大量应用于http的安全通信中,MQTT协议与http协议同样属于应用层协议,因此也可以像http协议一样使用ssl为自己的通信提供安全保证。
SSL与TLS(Transport LayerSecurity Protocol)之间的关系:TLS(TransportLayer Security,传输层安全协议)是IETF(InternetEngineering Task Force,Internet工程任务组)制定的一种新的协议,它建立在SSL 3.0协议规范之上,是SSL 3.0的后续版本。在TLS与SSL3.0之间存在着显著的差别,主要是它们所支持的加密算法不同,所以TLS与SSL3.0不能互操作。
2、使用Openssl创建tls证书
SSL在身份认证过程中需要有一个双方都信任的CA签发的证书,CA签发证书是需要收费的,但是在测试过程中,可以自己产生一个CA,然后用自己产生的CA签发证书,下面的mosquitto的ssl功能的测试过程就是采用这一方式,其过程如下:
步骤一:产生自己的CA
openssl req -new -x509 -days 36500 -extensions v3_ca -keyout ca.key -out ca.crt
openssl req -new -x509 -days 36500 -extensions v3_ca -keyout ca.key -out ca.pem
步骤二:产生服务端证书
openssl genrsa -des3 -out server.key 2048
openssl req -out server.csr -key server.key -new
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 36500
步骤三:产生客户端证书
openssl genrsa -out client-key.pem 2048
openssl req -out client.csr -key client-key.pem -new
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client-crt.pem -days 36500
经过上面8条命令后,即可生成所需的所有证书文件,其中:
客户端使用:ca.pem、client-crt.pem、client-key.pem
服务端使用:ca.crt、server.crt、server.key
3、mosquitto.conf配置如下:
4、golang客户端测试代码
1 package cmd 2 3 import ( 4 "crypto/tls" 5 "crypto/x509" 6 fmt "fmt" 7 "io/ioutil" 8 "os" 9 "time" 10 11 "github.com/apex/log" 12 MQTT "github.com/eclipse/paho.mqtt.golang" 13 ) 14 15 var ctx log.Interface 16 17 const QoS = 0x02 18 19 func init() { 20 fmt.Printf("init mqtt test\n") 21 22 } 23 24 func RunMqttClient() { 25 fmt.Printf("Run mqtt test\n") 26 var logLevel = log.InfoLevel 27 ctx = &log.Logger{ 28 Level: logLevel, 29 Handler: NewLogHanler(os.Stdout), 30 } 31 32 mqttClient := NewClient( 33 ctx, 34 "ttnhdl", 35 "", 36 "", 37 fmt.Sprintf("ssl://%s", "192.168.195.201:8883"), 38 ) 39 40 var err = mqttClient.Connect() 41 if err != nil { 42 ctx.WithError(err).Fatal("Could not connect to MQTT") 43 fmt.Printf("Could not connect to MQTT\n") 44 } else { 45 fmt.Printf("Success connect to MQTT\n") 46 } 47 48 mqttClient.PublishUplink("test", "hello mqtt!") 49 mqttClient.SubscribeUplink("test") 50 51 for true { 52 53 } 54 } 55 56 // Client connects to the MQTT server and can publish/subscribe on uplink, downlink and activations from devices 57 type Client interface { 58 Connect() error 59 Disconnect() 60 61 IsConnected() bool 62 63 // Uplink pub/sub 64 PublishUplink(topic string, msg string) Token 65 SubscribeUplink(topic string) Token 66 } 67 68 type Token interface { 69 Wait() bool 70 WaitTimeout(time.Duration) bool 71 Error() error 72 } 73 74 type simpleToken struct { 75 err error 76 } 77 78 // Wait always returns true 79 func (t *simpleToken) Wait() bool { 80 return true 81 } 82 83 // WaitTimeout always returns true 84 func (t *simpleToken) WaitTimeout(_ time.Duration) bool { 85 return true 86 } 87 88 // Error contains the error if present 89 func (t *simpleToken) Error() error { 90 return t.err 91 } 92 93 type defaultClient struct { 94 mqtt MQTT.Client 95 ctx log.Interface 96 } 97 98 func NewClient(ctx log.Interface, id, username, password string, brokers ...string) Client { 99 tlsconfig := NewTLSConfig() 100 101 mqttOpts := MQTT.NewClientOptions() 102 103 for _, broker := range brokers { 104 mqttOpts.AddBroker(broker) 105 } 106 107 mqttOpts.SetClientID("ypf_dewqfvcdeqfcdqwcdq") 108 mqttOpts.SetUsername(username) 109 mqttOpts.SetPassword(password) 110 111 // TODO: Some tuning of these values probably won't hurt: 112 mqttOpts.SetKeepAlive(30 * time.Second) 113 mqttOpts.SetPingTimeout(10 * time.Second) 114 115 // Usually this setting should not be used together with random ClientIDs, but 116 // we configured The Things Network's MQTT servers to handle this correctly. 117 mqttOpts.SetCleanSession(false) 118 119 mqttOpts.SetDefaultPublishHandler(func(client MQTT.Client, msg MQTT.Message) { 120 ctx.WithField("message", msg).Warn("Received unhandled message") 121 }) 122 123 mqttOpts.SetConnectionLostHandler(func(client MQTT.Client, err error) { 124 ctx.WithError(err).Warn("Disconnected, reconnecting...") 125 }) 126 127 mqttOpts.SetOnConnectHandler(func(client MQTT.Client) { 128 ctx.Debug("Connected") 129 }) 130 131 mqttOpts.SetTLSConfig(tlsconfig) 132 133 return &defaultClient{ 134 mqtt: MQTT.NewClient(mqttOpts), 135 ctx: ctx, 136 } 137 } 138 139 var ( 140 // ConnectRetries says how many times the client should retry a failed connection 141 ConnectRetries = 10 142 // ConnectRetryDelay says how long the client should wait between retries 143 ConnectRetryDelay = time.Second 144 ) 145 146 func (c *defaultClient) Connect() error { 147 if c.mqtt.IsConnected() { 148 return nil 149 } 150 var err error 151 for retries := 0; retries < ConnectRetries; retries++ { 152 token := c.mqtt.Connect() 153 token.Wait() 154 err = token.Error() 155 if err == nil { 156 break 157 } 158 <-time.After(ConnectRetryDelay) 159 } 160 if err != nil { 161 return fmt.Errorf("Could not connect: %s", err) 162 } 163 return nil 164 } 165 166 func (c *defaultClient) Disconnect() { 167 if !c.mqtt.IsConnected() { 168 return 169 } 170 c.mqtt.Disconnect(25) 171 } 172 173 func (c *defaultClient) IsConnected() bool { 174 return c.mqtt.IsConnected() 175 } 176 177 func (c *defaultClient) PublishUplink(topic string, msg string) Token { 178 return c.mqtt.Publish(topic, QoS, false, msg) 179 } 180 181 func (c *defaultClient) SubscribeUplink(topic string) Token { 182 return c.mqtt.Subscribe(topic, QoS, func(mqtt MQTT.Client, msg MQTT.Message) { 183 // Determine the actual topic 184 fmt.Printf("Success SubscribeUplink with msg:%s\n", msg.Payload()) 185 }) 186 } 187 188 func NewTLSConfig() *tls.Config { 189 // Import trusted certificates from CAfile.pem. 190 // Alternatively, manually add CA certificates to 191 // default openssl CA bundle. 192 certpool := x509.NewCertPool() 193 pemCerts, err := ioutil.ReadFile("samplecerts/ca.pem") 194 if err == nil { 195 certpool.AppendCertsFromPEM(pemCerts) 196 } 197 fmt.Println("0. resd pemCerts Success") 198 199 // Import client certificate/key pair 200 cert, err := tls.LoadX509KeyPair("samplecerts/client-crt.pem", "samplecerts/client-key.pem") 201 if err != nil { 202 panic(err) 203 } 204 fmt.Println("1. resd cert Success") 205 206 // Just to print out the client certificate.. 207 cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]) 208 if err != nil { 209 panic(err) 210 } 211 fmt.Println("2. resd cert.Leaf Success") 212 213 // Create tls.Config with desired tls properties 214 return &tls.Config{ 215 // RootCAs = certs used to verify server cert. 216 RootCAs: certpool, 217 // ClientAuth = whether to request cert from server. 218 // Since the server is set up for SSL, this happens 219 // anyways. 220 ClientAuth: tls.NoClientCert, 221 // ClientCAs = certs used to validate client cert. 222 ClientCAs: nil, 223 // InsecureSkipVerify = verify that cert contents 224 // match server. IP matches what is in cert etc. 225 InsecureSkipVerify: true, 226 // Certificates = list of certs client sends to server. 227 Certificates: []tls.Certificate{cert}, 228 } 229 }
5、测试效果
服务端启动:
客户端运行:
6、JAVA版客户端实现
依赖:org.eclipse.paho.client.mqttv3、bcprov-jdk16-1.45.jar
MqttServiceClient代码:
1 package com.ypf.main; 2 3 import java.util.Properties; 4 5 import org.eclipse.paho.client.mqttv3.MqttCallback; 6 import org.eclipse.paho.client.mqttv3.MqttClient; 7 import org.eclipse.paho.client.mqttv3.MqttConnectOptions; 8 import org.eclipse.paho.client.mqttv3.MqttDeliveryToken; 9 import org.eclipse.paho.client.mqttv3.MqttException; 10 import org.eclipse.paho.client.mqttv3.MqttMessage; 11 import org.eclipse.paho.client.mqttv3.MqttSecurityException; 12 import org.eclipse.paho.client.mqttv3.MqttTopic; 13 import org.eclipse.paho.client.mqttv3.internal.MemoryPersistence; 14 15 import com.ypf.mqtt.SslUtil; 16 17 /** 18 * 19 * @author LP by 2014-04-24 20 * 21 */ 22 public class MqttServiceClient implements MqttCallback { 23 24 private static final String MQTT_HOST = "ssl://192.168.195.201:8884"; 25 private static final String MQTT_CLIENT = "Test_"; 26 public static String caFilePath = "D:/for-iot/LURA/src/mytest/samplecerts/ca.crt"; 27 public static String clientCrtFilePath = "D:/for-iot/LURA/src/mytest/samplecerts/client.crt"; 28 public static String clientKeyFilePath = "D:/for-iot/LURA/src/mytest/samplecerts/client.key"; 29 30 public static MqttServiceClient mqttServiceClient = null; 31 32 private MqttClient client = null; 33 private MqttConnectOptions options = null; 34 35 /** 36 * 单例模式构造类 37 */ 38 public static MqttServiceClient getInstance() { 39 if (mqttServiceClient == null) { 40 mqttServiceClient = new MqttServiceClient(); 41 } 42 return mqttServiceClient; 43 } 44 45 private MqttServiceClient() { 46 System.out.println("init MQTTClientService"); 47 init(); 48 } 49 // The major API implementation follows :- 50 51 /** 52 * 初始化 53 */ 54 private void init() { 55 try { 56 57 // host为主机名,test为clientid即连接MQTT的客户端ID,一般以客户端唯一标识符表示,MemoryPersistence设置clientid的保存形式,默认为以内存保存 58 client = new MqttClient(MQTT_HOST, MQTT_CLIENT, new MemoryPersistence()); 59 // MQTT的连接设置 60 options = new MqttConnectOptions(); 61 // 设置是否清空session,这里如果设置为false表示服务器会保留客户端的连接记录,这里设置为true表示每次连接到服务器都以新的身份连接 62 options.setCleanSession(true); 63 // 设置连接的用户名 64 options.setUserName("ypf"); 65 // 设置连接的密码 66 options.setPassword("ruijie".toCharArray()); 67 // 设置超时时间 单位为秒 68 options.setConnectionTimeout(50); 69 // 设置会话心跳时间 单位为秒 服务器会每隔1.5*20秒的时间向客户端发送个消息判断客户端是否在线,但这个方法并没有重连的机制 70 options.setKeepAliveInterval(30); 71 // TLS连接配置 72 options.setSocketFactory( 73 SslUtil.getSocketFactory(caFilePath, clientCrtFilePath, clientKeyFilePath, "cs123456")); 74 75 // 设置回调 76 client.setCallback(this); 77 78 } catch (Exception e) { 79 e.printStackTrace(); 80 } 81 } 82 /** 83 * 连接到MQTT 84 */ 85 void connect() { 86 System.out.println("Start connect----------"); 87 try { 88 client.connect(options); 89 //订阅主题的方法,2为消息的质量 90 client.subscribe("+/#", 2); 91 //发送消息 92 publish("test", "撒打发水电费水电费"); 93 } catch (Exception e) { 94 e.printStackTrace(); 95 } 96 } 97 98 /** 99 * 断开连接到MQTT 100 */ 101 public void disconnect() { 102 System.out.println("Start disconnect----------"); 103 try { 104 client.disconnect(); 105 } catch (MqttSecurityException e) { 106 e.printStackTrace(); 107 } catch (MqttException e) { 108 e.printStackTrace(); 109 } 110 } 111 112 /** 113 * 发布消息 114 * @param topic 主题 115 * @param msg 消息 116 */ 117 public void publish(String topic, String msg) { 118 System.out.println("Start publish----------"); 119 try { 120 MqttTopic mqttTopic = client.getTopic(topic); 121 //2为消息的质量 122 MqttDeliveryToken messageToken = mqttTopic.publish(msg.getBytes(), 2, true); 123 System.out.println("publish success==>"+messageToken.getMessage()); 124 // client.publish(topic, 2, msg); 125 } catch (Exception e) { 126 e.printStackTrace(); 127 } 128 } 129 130 131 // -------------------------------------------------回调方法------------------------------------------------------------// 132 133 /** 134 * 连接断开触发此方法 135 */ 136 @Override 137 public void connectionLost(Throwable cause) { 138 System.out.println("Connection Lost---------->" + cause.getMessage()); 139 } 140 141 /** 142 * 消息达到触发此方法 143 */ 144 @Override 145 public void messageArrived(MqttTopic topic, MqttMessage message) 146 throws Exception { 147 System.out.println(topic + ":" + message.toString()); 148 } 149 150 /** 151 * 消息发送成功触发此方法 152 */ 153 @Override 154 public void deliveryComplete(MqttDeliveryToken token) { 155 try { 156 System.out.println("deliveryComplete---------" + token.getMessage()); 157 } catch (MqttException e) { 158 e.printStackTrace(); 159 } 160 } 161 162 163 public static void main(String[] args)throws Exception { 164 165 //MqttServiceClient.getInstance().disconnect(); 166 MqttServiceClient.getInstance().connect(); 167 168 new Thread() { 169 public void run() { 170 int count = 0; 171 while(true && count < 3) { 172 try { 173 Thread.sleep(1000*3); 174 } catch (InterruptedException e) { 175 e.printStackTrace(); 176 } 177 MqttServiceClient.getInstance().publish("test1/ypf", "hello world ! count=" + count); 178 count ++; 179 } 180 }; 181 }.start(); 182 } 183 184 }
SslUtil代码:
1 package com.ypf.mqtt; 2 3 import java.io.ByteArrayInputStream; 4 import java.io.InputStreamReader; 5 import java.nio.file.Files; 6 import java.nio.file.Paths; 7 import java.security.KeyPair; 8 import java.security.KeyStore; 9 import java.security.Security; 10 import java.security.cert.X509Certificate; 11 12 import javax.net.ssl.KeyManagerFactory; 13 import javax.net.ssl.SSLContext; 14 import javax.net.ssl.SSLSocketFactory; 15 import javax.net.ssl.TrustManagerFactory; 16 17 import org.bouncycastle.jce.provider.BouncyCastleProvider; 18 import org.bouncycastle.openssl.*; 19 20 public class SslUtil { 21 public static SSLSocketFactory getSocketFactory(final String caCrtFile, final String crtFile, final String keyFile, 22 final String password) throws Exception { 23 Security.addProvider(new BouncyCastleProvider()); 24 25 // load CA certificate 26 PEMReader reader = new PEMReader( 27 new InputStreamReader(new ByteArrayInputStream(Files.readAllBytes(Paths.get(caCrtFile))))); 28 X509Certificate caCert = (X509Certificate) reader.readObject(); 29 reader.close(); 30 31 // load client certificate 32 reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(Files.readAllBytes(Paths.get(crtFile))))); 33 X509Certificate cert = (X509Certificate) reader.readObject(); 34 reader.close(); 35 36 // load client private key 37 reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(Files.readAllBytes(Paths.get(keyFile)))), 38 new PasswordFinder() { 39 @Override 40 public char[] getPassword() { 41 return password.toCharArray(); 42 } 43 }); 44 KeyPair key = (KeyPair) reader.readObject(); 45 reader.close(); 46 47 // CA certificate is used to authenticate server 48 KeyStore caKs = KeyStore.getInstance(KeyStore.getDefaultType()); 49 caKs.load(null, null); 50 caKs.setCertificateEntry("ca-certificate", caCert); 51 TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 52 tmf.init(caKs); 53 54 // client key and certificates are sent to server so it can authenticate 55 // us 56 KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); 57 ks.load(null, null); 58 ks.setCertificateEntry("certificate", cert); 59 ks.setKeyEntry("private-key", key.getPrivate(), password.toCharArray(), 60 new java.security.cert.Certificate[] { cert }); 61 KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); 62 kmf.init(ks, password.toCharArray()); 63 64 // finally, create SSL socket factory 65 SSLContext context = SSLContext.getInstance("TLSv1"); 66 context.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); 67 68 return context.getSocketFactory(); 69 } 70 }