Paho是Eclipse提供的,一个用Java编写的MQTT客户端的库。
本文连接云端使用的是X509证书方式连接。
一、配置Paho库
首先引入Paho:
api 'org.eclipse.paho:org.eclipse.paho.android.service:1.1.1'
api 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5'
权限申请增加:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
添加service:
<service android:name="org.eclipse.paho.android.service.MqttService" />
做完这些后,就可以使用Paho了。
二、连接AWS IOT
然后我们来使用Paho连接AWS,首先贴代码:
MqttConnectOptions options = new MqttConnectOptions();
SocketFactory socketFactory = getSocketFactory(context);
if(socketFactory == null){
return;
}
options.setSocketFactory(socketFactory);
options.setCleanSession(true);
options.setConnectionTimeout(15);
options.setKeepAliveInterval(60);
try {
MqttAndroidClient client = new MqttAndroidClient(context,
"ssl://XXX:8883",
"MQTT_Test");
client.connect(options, null, new IMqttActionListener() {
@Override
public void onSuccess(IMqttToken asyncActionToken) {
Log.i(TAG, "连接成功 ");
}
@Override
public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
Log.i(TAG, "连接失败 :" + exception);
}
});
} catch (MqttException e) {
e.printStackTrace();
}
这一段代码主要做了两件事,第一件构建MqttConnectOptions,然后使用MqttConnectOptions作为参数连接AWS IOT。
1.构建MqttConnectOptions:这里对MqttConnectOptions设置了SocketFactory(因为是X509的认证方式,所以会使用到SocketFactory)、CleanSession(设置客户端和服务器是否应在重新启动和重新连接时记住状态)、ConnectionTimeout(设置连接超时时间,单位为秒)、KeepAliveInterval(设置“保持活动”间隔,单位为秒,按照设置的时间为间隔会一直去检测连接是否可用)。
2.连接IOT:初始化MqttAndroidClient,需要Context,ServiceURL(由"ssl://" + EndPoint + ":8883",这里的8883端口号根据实际情况设置),ClientID(这里可以随便设置,但在相同的证书,相同的地址的情况下,ClientID在同一时间不可重复),最后使用MqttConnectOptions作为参数、设置连接状态监听便可开始连接。
第二点连接也可以初始化MqttClient,然后使用connect连接。
MqttClient client = new MqttClient("ssl://XXX:8883",
"test", new MemoryPersistence());
client.connect(options);
上面的代码对所有的MQTT连接应该都是适用,但是每一个云端平台应该都有不一样的地方,这个不一样的地方就在SocketFactory。下面就介绍SocketFactory的生成。
三、SocketFactory的生成
SocketFactory可以由SSLContext取得,SSLContext的构造离不开KeyStore,所以怎么将X509证书放入KeyStore,成了关键,KeyStore创建有问题,前面的连接也会有问题。
X509证书有两种情况,一个是直接就知道证书私钥的内容,一个是写入到一个文件中无法知道证书私钥的内容。
1.有明文的证书、私钥
这里指已经知道证书、私钥的内容,但是没有封装好的文件的情况。这时呢需要将证书以及私钥封装成Certificate以及Key使用。下面的封装方法来自微软。
需要导入这些库用来封装:
api 'org.bouncycastle:bcprov-jdk15on:1.66'
api 'org.bouncycastle:bcpkix-jdk15on:1.66'
证书内容封装成Certificate:
private X509Certificate buildCertificate(String certificateString) throws CertificateException
{
try
{
Security.addProvider(new BouncyCastleProvider());
PemReader publicKeyCertificateReader = new PemReader(new StringReader(certificateString));
PemObject possiblePublicKeyCertificate = publicKeyCertificateReader.readPemObject();
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
return (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(possiblePublicKeyCertificate.getContent()));
}
catch (Exception e)
{
throw new CertificateException(e);
}
}
私钥内容封装成Key:
private Key buildKey(String keyString){
try
{
Security.addProvider(new BouncyCastleProvider());
PEMParser privateKeyParser = new PEMParser(new StringReader(keyString));
Object possiblePrivateKey = privateKeyParser.readObject();
return getPrivateKey(possiblePrivateKey);
}
catch (Exception e)
{
e.printStackTrace();
}
return null;
}
private Key getPrivateKey(Object possiblePrivateKey) throws IOException
{
if (possiblePrivateKey instanceof PEMKeyPair)
{
return new JcaPEMKeyConverter().getKeyPair((PEMKeyPair) possiblePrivateKey)
.getPrivate();
}
else if (possiblePrivateKey instanceof PrivateKeyInfo)
{
return new JcaPEMKeyConverter().getPrivateKey((PrivateKeyInfo) possiblePrivateKey);
}
else
{
throw new IOException("Unable to parse private key, type unknown");
}
}
2.已经有证书文件
这里以pkcs12文件为例,从文件中取出Certificate以及Key:
public void getCertAndKey(Context context){
try {
String password = "password";
InputStream assetsIn = context.getAssets().open("test.pkcs12");
KeyStore inStore = KeyStore.getInstance("PKCS12");
inStore.load(assetsIn, password.toCharArray());
Enumeration enums = inStore.aliases();
while (enums.hasMoreElements()) {
String keyAlias = (String) enums.nextElement();
if (inStore.isKeyEntry(keyAlias)) {
Key key = inStore.getKey(keyAlias, password.toCharArray());
Certificate[] certChain = inStore.getCertificateChain(keyAlias);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
从文件里面可以使用KeyStore取出Certificate以及Key,需要密码(生成pkcs12文件时设置的密码)。
以上两种方式可以得到Certificate以及Key,有两个类就可以开始构建KeyStore:
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null);
keyStore.setCertificateEntry("cert-alias", certChain[0]);
keyStore.setKeyEntry("key-alias", key, "password".toCharArray(), certChain);
这里的key以及certChain就是上面生成的(有明码的证书以及私钥构建的是X509Certificate,可以直接创建一个Certificate[],然后将X509Certificate放入位置为0的地方)。
有了KeyStore就可以创建SSLContext了,下面来创建SSLContext:
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory
.getDefaultAlgorithm());
kmf.init(keyStore, "password".toCharArray());
KeyManager[] km = kmf.getKeyManagers();
sc.init(km, null, new SecureRandom());
在KeyManagerFactory.init的时候使用的密码是创建KeyStore时设置的密码
这样SSLContext创建完成,就可以直接使用SSLContext的方法获取到SocketFactory:
sc.getSocketFactory()
到这里SocketFactory就已经有了,就可以做为参数设置到MqttConnectOptions中。
四、遇到的问题
因为我是先写的Azure云的连接,所以之后写AWS的连接时,就直接用了Azure的方式建立了SocketFactory,结果一直报
SSL routines:OPENSSL_internal:SSLV3_ALERT_BAD_CERTIFICATE
这个错误,它的意思是提示证书错误。
因为Azure构建KeyStore时,setCertificateEntry方法里面用的证书并不是我们之前所用到的明文的证书,是其他的证书,所以AWS我直接使用RootCA证书来代替Azure这里的证书,就一直提示证书错误,其实这里使用的证书与接下来setKeyEntry方法设置的证书是一样的。