java
用ssl
加密方式连接mqtt
服务器。其它ssl
加密的也可以参考,SSLSocketFactory
获取部分都是一样的。踩了很多坑,根据生成工具不同(openssl
和keytool
)以及秘钥文件编码不同有若干种方法。这里把自己遇到的所有情况都统一记录一下。
一、连接MQTT服务器
不加密的连接方式之前有写过,就不赘述了,这里列出不同的地方
mqttClient = new MqttClient(host, clientId, new MemoryPersistence());
MqttConnectOptions options = new MqttConnectOptions();
options.setCleanSession(true);
// 这里多了一步设置SSLSocketFactory的步骤
options.setSocketFactory(SslUtil.getSocketFactoryByCert(caPath,certPath,privateKeyPath, privateKeyPwd));
SSLSocketFactory获取方式有两种:
- 通过
CA
证书、客户端证书、客户端私钥、私钥密码 获取(使用openssl
生成的,keytool
能生成证书,但是不能直接导出秘钥文件) - 直接通过
keystore
和truststore
获取(通过keytool
生成的)
读取证书和秘钥也有两种方式(证书获取的方式)
- 使用
bcpkix-jdk15on
包提供的方法,需要引包 - 使用原生方法,但是不支持直接读取
pom
秘钥文件,需要先把文件PKCS8
编码一下。(编码方法在openssl
的文章里)
稍微解释一下上面的两种方式
- 第一种,通过证书的方式
-
CA
证书是用来验证服务端发过来的证书,因为这里是双向认证,所以需要CA
证书来认证服务端发过来的是否是合法证书。 - 客户端证书,发给服务端,让服务端验证的。(需要用
CA
证书签发,这样服务端那边才能用CA
证书验证合法) - 客户端私钥,服务端拿到客户端证书后会用证书里的公钥加密信息发过来,需要用私钥解密拿到原信息
- 私钥密码,
openssl
生成私钥的时候设置的密码(具体生成方式之前的文章有)
- 第二种,通过
keystore
和truststore
-
keystore
是用jdk
自带的工具keytool
生成的秘钥和证书管理库,用来保存自己的秘钥和证书。需要用keytool
生成并导入客户端的证书和秘钥。具体使用之前有文章可以参考。 -
truststore
本质也是keystore
,只是里面存的是受信的证书。用来验证服务端证书是否可信,将CA
导入即可 - 第一种方式本质也是通过
keystore
和truststore
验证,只不过导入的步骤用代码实现了,第二种方式使用命令实现的。
二、SslUtil具体实现
- 导入依赖
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.47</version>
</dependency>
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();
}
}
三、不引包的方式
- 将
pem
秘钥文件pkcs8
编码
openssl pkcs8 -topk8 -in client.private.pem -out pkcs8.client.private.pem -nocrypt
- 代码
/**
* 用证书和私钥配置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
证书验证的方式
/**
* 通过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();
}
- 单向认证,即不认证服务端的证书
// 自定义一个不验证的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是否匹配
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());
关键词: java
,ssl
,mqtt
网友评论