0%

java连接MQTT SSL服务器

文章字数:1467,阅读全文大约需要5分钟

javassl加密方式连接mqtt服务器。其它ssl加密的也可以参考,SSLSocketFactory获取部分都是一样的。踩了很多坑,根据生成工具不同(opensslkeytool)以及秘钥文件编码不同有若干种方法。这里把自己遇到的所有情况都统一记录一下。

一、连接MQTT服务器

不加密的连接方式之前有写过,就不赘述了,这里列出不同的地方

1
2
3
4
5
mqttClient = new MqttClient(host, clientId, new MemoryPersistence());
MqttConnectOptions options = new MqttConnectOptions();
options.setCleanSession(true);
// 这里多了一步设置SSLSocketFactory的步骤
options.setSocketFactory(SslUtil.getSocketFactoryByCert(caPath,certPath,privateKeyPath, privateKeyPwd));

SSLSocketFactory获取方式有两种:

  1. 通过CA证书、客户端证书、客户端私钥、私钥密码 获取(使用openssl生成的,keytool能生成证书,但是不能直接导出秘钥文件)
  2. 直接通过keystoretruststore获取(通过keytool生成的)

读取证书和秘钥也有两种方式(证书获取的方式)

  1. 使用bcpkix-jdk15on包提供的方法,需要引包
  2. 使用原生方法,但是不支持直接读取pem秘钥文件,需要先把文件PKCS8编码一下。(编码方法在openssl的文章里)

稍微解释一下上面的两种方式

  • 第一种,通过证书的方式
  1. CA证书是用来验证服务端发过来的证书,因为这里是双向认证,所以需要CA证书来认证服务端发过来的是否是合法证书。
  2. 客户端证书,发给服务端,让服务端验证的。(需要用CA证书签发,这样服务端那边才能用CA证书验证合法)
  3. 客户端私钥,服务端拿到客户端证书后会用证书里的公钥加密信息发过来,需要用私钥解密拿到原信息
  4. 私钥密码,openssl生成私钥的时候设置的密码(具体生成方式之前的文章有)
  • 第二种,通过keystoretruststore
  1. keystore是用jdk自带的工具keytool生成的秘钥和证书管理库,用来保存自己的秘钥和证书。需要用keytool生成并导入客户端的证书和秘钥。具体使用之前有文章可以参考。
  2. truststore本质也是keystore,只是里面存的是受信的证书。用来验证服务端证书是否可信,将CA导入即可
  3. 第一种方式本质也是通过keystoretruststore验证,只不过导入的步骤用代码实现了,第二种方式使用命令实现的。

二、SslUtil具体实现

  1. 导入依赖
    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcpkix-jdk15on</artifactId>
    <version>1.47</version>
    </dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMReader;

/***
* 两种方式验证
* @author colin
* @date 2021-02-03 14:39
* @since 1.0.0
*/
public class SslUtil {

/**
* 用证书和私钥配置sslContext
*
* @param caCrtFile
* CA证书(验证连接)
* @param crtFile
* 发给对方的证书
* @param keyFile
* pem 私钥(请求连接的消息是用公钥加密的,需要用私钥解密)
* @param password
* 私钥密码
* @return
* @throws Exception
*/
public static SSLSocketFactory getSocketFactoryByCert(final String caCrtFile, final String crtFile,
final String keyFile, final String password) throws Exception {
Security.addProvider(new BouncyCastleProvider());

// 加载CA证书(用于验证的根证书)
PEMReader reader =
new PEMReader(new InputStreamReader(new ByteArrayInputStream(Files.readAllBytes(Paths.get(caCrtFile)))));
X509Certificate caCert = (X509Certificate)reader.readObject();
reader.close();

// 加载自己的证书,用于发送给客户端
reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(Files.readAllBytes(Paths.get(crtFile)))));
X509Certificate cert = (X509Certificate)reader.readObject();
reader.close();

// 加载私钥
reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(Files.readAllBytes(Paths.get(keyFile)))),
() -> password.toCharArray());
KeyPair key = (KeyPair)reader.readObject();
reader.close();

// 用CA证书创建TrustManagerFactory
KeyStore caKs = KeyStore.getInstance(KeyStore.getDefaultType());
caKs.load(null, null);
caKs.setCertificateEntry("ca-certificate", caCert);
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(caKs);

// 用证书和私钥创建KeyManagerFactory
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null, null);
ks.setCertificateEntry("certificate", cert);
ks.setKeyEntry("private-key", key.getPrivate(), password.toCharArray(),
new java.security.cert.Certificate[] {cert});
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, password.toCharArray());

SSLContext context = SSLContext.getInstance("TLSv1");
// kmf用于发送关键信息让服务端校验,tmf用于校验服务端的证书。双向认证
context.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());
return context.getSocketFactory();
}

/**
* 通过keyStore加载
*
* @param keyStorePath
* keystore路径(保存自己的秘钥和证书)
* @param trustKeyStorePath
* truststore路径(保存受信的证书)
* @param ksPass
* keystore密码
* @param tsPass
* truststore密码
* @return
* @throws Exception
*/
public static SSLSocketFactory getSocketFactoryByKeystore(String keyStorePath, String trustKeyStorePath,
String ksPass, String tsPass) throws Exception {
// keytool生成的keystore的类型就是JKS
KeyStore keyStore = KeyStore.getInstance("JKS");
KeyStore trustKeyStore = KeyStore.getInstance("JKS");
// 通过密码加载keystore
FileInputStream fis = new FileInputStream(keyStorePath);
keyStore.load(fis, ksPass.toCharArray());
fis.close();
// 加载trustKeyStore
FileInputStream trustFis = new FileInputStream(trustKeyStorePath);
trustKeyStore.load(trustFis, tsPass.toCharArray());
trustFis.close();
// 创建管理JKS密钥库的密钥管理器 (SunX509)
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
// 使用密钥内容源初始化此工厂。 提供者通常使用 KeyStore 来获取在安全套接字协商期间所使用的密钥内容
kmf.init(keyStore, ksPass.toCharArray());
// SunX509
TrustManagerFactory tmFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmFactory.init(trustKeyStore);

// 初始sslcontext
SSLContext sslContext = SSLContext.getInstance("SSLv3");
// SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), tmFactory.getTrustManagers(), new SecureRandom());
return sslContext.getSocketFactory();
}

}

三、不引包的方式

  1. pem秘钥文件pkcs8编码
1
openssl pkcs8 -topk8 -in client.private.pem -out pkcs8.client.private.pem -nocrypt
  1. 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/**
* 用证书和私钥配置sslContext
*
* @param caCrtFile
* CA证书(验证连接)
* @param crtFile
* 发给对方的证书
* @param keyFile
* 私钥(请求连接的消息是用公钥加密的,需要用私钥解密)
* @param password
* 私钥密码
* @return
* @throws Exception
*/
public static SSLSocketFactory getSocketFactoryByCert(final String caCrtFile, final String crtFile,
final String keyFile, final String password) throws Exception {
// 加载CA证书(用于验证的根证书)
X509Certificate caCert = getCertificate(caCrtFile);
// 加载自己的证书,用于发送给客户端
X509Certificate cert = getCertificate(crtFile);
// 加载私钥
final PrivateKey privateKey = getPrivateKey(keyFile);
// 用CA证书创建TrustManagerFactory
KeyStore caKs = KeyStore.getInstance(KeyStore.getDefaultType());
caKs.load(null, null);
caKs.setCertificateEntry("ca-certificate", caCert);
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(caKs);

// 用证书和私钥创建KeyManagerFactory
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null, null);
ks.setCertificateEntry("certificate", cert);
ks.setKeyEntry("private-key", privateKey, password.toCharArray(), new java.security.cert.Certificate[] {cert});
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, password.toCharArray());

SSLContext context = SSLContext.getInstance("TLSv1");
// kmf用于发送关键信息让服务端校验,tmf用于校验服务端的证书。双向认证
context.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());
return context.getSocketFactory();
}

/**
* 读取x509格式的证书
*
* @param certPath
* @return
* @throws FileNotFoundException
* @throws CertificateException
*/
private static X509Certificate getCertificate(String certPath) throws FileNotFoundException, CertificateException {
InputStream inStream = new FileInputStream(certPath);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
X509Certificate caCert = (X509Certificate)cf.generateCertificate(inStream);
inStream.close();
return caCert;
}

/**
* 读取 PKCS8 编码的 RSA 秘钥文件
*
* @param path
* @return
* @throws IOException
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
*/
private static PrivateKey getPrivateKey(String path)
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
BufferedReader br = new BufferedReader(new FileReader(path));
String s = br.readLine();
String str = "";
s = br.readLine();
while (s.charAt(0) != '-') {
str += s + "\r";
s = br.readLine();
}
// BASE64Decoder base64decoder = new BASE64Decoder();
byte[] bytes = Base64.getMimeDecoder().decode(str);
// byte[] bytes = base64decoder.decodeBuffer(str);
br.close();
// 生成私钥
KeyFactory kf = KeyFactory.getInstance("RSA");
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes);
PrivateKey privateKey = kf.generatePrivate(keySpec);
return privateKey;
}

发现项目中有生成好的p12证书,可以直接使用。这里再追加一种p12证书和CA证书验证的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* 通过p12证书和ca证书双向认证
*
* @param caCrtFile
* @param p12Keystore
* @param p12Pwd
* @return
* @throws Exception
*/
public static SSLSocketFactory getSocketFactoryByP12AndCA(String caCrtFile, String p12Keystore, String p12Pwd)
throws Exception {
// 加载CA证书(用于验证的根证书)
X509Certificate caCert = getCertificate(caCrtFile);
// 用CA证书创建TrustManagerFactory
KeyStore caKs = KeyStore.getInstance(KeyStore.getDefaultType());
caKs.load(null, null);
caKs.setCertificateEntry("ca-certificate", caCert);
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(caKs);

KeyStore keyStore = KeyStore.getInstance("pkcs12");
FileInputStream p12Fis = new FileInputStream(p12Keystore);
keyStore.load(p12Fis , p12Pwd.toCharArray());
p12Fis.close();
// 创建管理JKS密钥库的密钥管理器 (SunX509)
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
// 使用密钥内容源初始化此工厂。 提供者通常使用 KeyStore 来获取在安全套接字协商期间所使用的密钥内容
kmf.init(keyStore, p12Pwd.toCharArray());

// 初始sslcontext
SSLContext sslContext = SSLContext.getInstance("SSLv3");
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());
return sslContext.getSocketFactory();
}
  • 单向认证,即不认证服务端的证书
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 自定义一个不验证的TrustManager 即可
final TrustManager trustManager = new X509TrustManager(){
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {

}

@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {

}

@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
};
sslContext.init(kmf.getKeyManagers(), new TrustManager[] {trustManager}, new SecureRandom());
  • 不验证证书及ip是否匹配
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
final TrustManager trustManager = new X509ExtendedTrustManager(){

@Override
public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException {

}

@Override
public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException {

}

@Override
public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException {

}

@Override
public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException {

}

@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {

}

@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {

}

@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
};
sslContext.init(kmf.getKeyManagers(), new TrustManager[] {trustManager}, new SecureRandom());