Loading

java连接MQTT+SSL服务器

摘自:https://www.jianshu.com/p/f498cd767d4b

java连接MQTT+SSL服务器

策马踏清风IP属地: 福建
2021.02.09 18:27:11字数 744阅读 4,328

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

一、连接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获取方式有两种:

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

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

  1. 使用bcpkix-jdk15on包提供的方法,需要引包
  2. 使用原生方法,但是不支持直接读取pom秘钥文件,需要先把文件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. 导入依赖
<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();
    }

}

三、不引包的方式

  1. pem秘钥文件pkcs8编码
openssl pkcs8 -topk8 -in client.private.pem -out pkcs8.client.private.pem -nocrypt
  1. 代码
/**
     * 用证书和私钥配置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

posted @ 2024-01-12 13:34  Sam Xiao  阅读(22)  评论(0编辑  收藏  举报