java实现大文件分段下载-私有证书双向认证版记录

之前整理过一篇java大文件断点续传下载分段下载nginx206问题
下面补充一下双向认证版本,实现代码如下:



import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.security.KeyFactory;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import javax.xml.bind.DatatypeConverter;

import com.google.common.base.Objects;
import com.ly.mp.springcloudnacos.nacos.NacosConfigs;

import ly.mp.project.common.ota.HashAlgorithm;
import ly.mp.project.common.util.LogUtils;

public class DownloadClientByCert {

    /**
     * 断点下载
     * @param num 
     * 
     * @throws InterruptedException
     */
    private static int execDownload(int num, String fileUrl, String targetPath, String caCertBase64, String clientCertBase64, String clientKeyBase64) throws Exception {
        SSLContext sslContext = getSSLContext(caCertBase64, clientCertBase64, clientKeyBase64);
        // 创建URL对象
        URL url = new URL(fileUrl);
        // 使用url获取HttpURLConnection对象
        HttpsURLConnection  conn = (HttpsURLConnection ) url.openConnection();
        conn.setSSLSocketFactory(sslContext.getSocketFactory());
        conn.setConnectTimeout(6000);
        conn.setReadTimeout(1000*60);
        // 客户端的请求方式
        conn.setRequestMethod("GET");
        // 已经下载的字节数
        long alreadySize = 0;
        // 将文件写到download/file.apk中
        File file = new File(targetPath);
        // 如果存在,说明原来下载过,不过可能没有下载完
        if (file.exists()) {
            // 如果文件存在,就获取当前文件的大小
            alreadySize = file.length();
        }
        /**
         * Range头域可以请求实体的一个或者多个子范围。 例如: 表示头500个字节:bytes=0-499
         * 表示第二个500字节:bytes=500-999 表示最后500个字节:bytes=-500
         * 表示500字节以后的范围:bytes=500- 第一个和最后一个字节:bytes=0-0,-1
         * 同时指定几个范围:bytes=500-600,601-999
         * 但是服务器可以忽略此请求头,如果无条件GET包含Range请求头,响应会以状态码206(PartialContent)返回而不是以200
         * (OK)。
         */
        conn.addRequestProperty("range", "bytes=" + alreadySize + "-" + (alreadySize+15*1024*1024));
        conn.connect();
        
        LogUtils.info("bytes=" + alreadySize + "-" + (alreadySize+15*1024*1024));

        // 206,一般表示断点续传
        // 获取服务器回馈的状态码
        int code = conn.getResponseCode();
        LogUtils.info("下载返回code:{}", code);
        // 如果响应成功,因为使用了range请求头,那么响应成功的状态码为206,而不是200
        if (code == 206) {
            // 获取未下载的文件的大小
            //bytes 12582915-16777219/5014745104
            String contentRange = conn.getHeaderField("Content-Range");
            LogUtils.info("contentRange:{}", contentRange);
           // long unfinishedSize = conn.getContentLengthLong();
            long totalSize = Long.parseLong(contentRange.substring(contentRange.lastIndexOf("/")+1));
            LogUtils.info("totalSize:{}", totalSize);

            // 获取输入流
            InputStream in = conn.getInputStream();
            // 获取输出对象,参数一:目标文件,参数2表示在原来的文件中追加
            try(OutputStream out = new BufferedOutputStream(new FileOutputStream(file, true));){
                // 开始下载
                byte[] buff = new byte[1024*1024*2];
                int len;
                while ((len = in.read(buff)) != -1) {
                    out.write(buff, 0, len);
                    // 将下载的累加到alreadSize中
                    alreadySize += len;
                    // 下载进度
                    int process =  (int)(alreadySize * 1.0 / totalSize * 100);
                    int lastProcess = (int)((alreadySize-len) * 1.0 / totalSize * 100);
                    if(process % 10 == 0 && lastProcess % 10 != 0) {
                        LogUtils.info("下载进度:" + process + " alreadySize:" + alreadySize + " totalSize:" + totalSize);
                    }
                }
                //out.close();
            }
            LogUtils.info("第" + (num+1) + "次下载完成!!!");
            if(alreadySize != 0 && !Objects.equal(alreadySize, totalSize) && alreadySize < totalSize) {
                num = num + 1;
            } else if(alreadySize != 0 && Objects.equal(alreadySize, totalSize)) {
                num = 0;
            }
        } else {
            LogUtils.info("下载失败!!!");
            num = 0;
        }

        // 断开连接
        conn.disconnect();
        return num;
    }
    
    // -------- 工具方法:解析Base64 PEM格式的证书/私钥 --------

    private static SSLContext getSSLContext(String caCertBase64, String clientCertBase64, String clientKeyBase64) throws Exception {
        // 1. 解析证书和私钥
        X509Certificate caCert = parseCertificate(caCertBase64);
        X509Certificate clientCert = parseCertificate(clientCertBase64);
//        PrivateKey clientPrivateKey = parsePrivateKey(clientKeyBase64);
        RSAPrivateKey clientPrivateKey = (RSAPrivateKey) HttpRequestUtil.parseKey(clientKeyBase64, null);

        // 2. 构建客户端密钥库 (KeyStore) - 存放客户端证书+私钥
        KeyStore clientKeyStore = KeyStore.getInstance("PKCS12");
        clientKeyStore.load(null, null); // 初始化空库
        String pass = "";
        try {
            pass = NacosConfigs.getPropDefaultIfBlank("cert.random.password", "123456");
        }catch (Exception e){
            pass = "123456";
        }
        // 将客户端证书链和私钥存入,别名随意
        clientKeyStore.setKeyEntry("client-alias-client", clientPrivateKey, pass.toCharArray(), 
                                   new java.security.cert.Certificate[]{clientCert});

        // 3. 构建信任库 (TrustStore) - 存放CA根证书
        KeyStore trustStore = KeyStore.getInstance("JKS");
        trustStore.load(null, null);
        trustStore.setCertificateEntry("ca-alias-client", caCert);

        // 4. 初始化 KeyManagerFactory 和 TrustManagerFactory
        KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        kmf.init(clientKeyStore, pass.toCharArray());

        TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        tmf.init(trustStore);

        // 5. 创建 SSLContext
        //根据你的jdk版本决定是TLS、TLSv1.1、TLSv1.2
        SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
        sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
        return sslContext;
    }

    /**
     * 将Base64编码的PEM证书字符串解析为 X509Certificate 对象
     * 注意:传入的字符串应只包含纯Base64内容,不要有 "-----BEGIN CERTIFICATE-----" 等头尾
     */
    private static X509Certificate parseCertificate(String base64Pem) throws Exception {
//        byte[] decoded = Base64.getDecoder().decode(base64Pem);
        CertificateFactory cf = CertificateFactory.getInstance("X.509");
//        return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(decoded));
        
        X509Certificate cert = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(DatatypeConverter.parseBase64Binary(base64Pem)));
        return cert;
    }

    /**
     * 将Base64编码的PEM私钥字符串解析为 PrivateKey 对象(支持PKCS#8格式)
     * 注意:传入的字符串应只包含纯Base64内容,不要有 "-----BEGIN PRIVATE KEY-----" 等头尾
     */
    private static PrivateKey parsePrivateKey(String base64Pem) throws Exception {
        byte[] decoded = Base64.getDecoder().decode(base64Pem);
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decoded);
        KeyFactory kf = KeyFactory.getInstance("RSA"); // 如果是EC证书,需改为 "EC"
        return kf.generatePrivate(keySpec);
    }
    
    public static void main(String[] args) {
        String fileUrl = "https://xx.com.cn/xx.iso";
        String targetPath = "D://tmp/xx.iso";
        
        
        String caCertBase64 = "MIIErTCCApWgAwIBAgIIHXlyJYQfgoYwDQTEXMBUGA1UEAwwOREZMX1BWIFJvb3Qg\n"
                + "BhMCQ04xEjAQBgNVBAoMCURGTF9QVl9DQTEXMBUGA1UEAwwOREZMX1BWIFJvb3Qg\n"
                + "Q0EwIBcNMjAxMDIxMDMxMTE3WhgPMjA1MDAxMTIwODEwMTFaMEcxCzAJBgNVBAYT\n"
                + "AkNOMRkwFwYDVQQKDBBERkxfUFZfTklTU0FOX0NBMR0wGwYDVQQDDBRERkxfUFZf\n"
                + "TklTU0FOIFN1YiBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALzW\n"
                + "v7d6cEH6IR3WpvcxDREBOtMKp/3xdIWKxye0h4x9aBBavAt9bhGyV9v541eK0K50\n"
                + "w8fkh/z/RNPgoKgD17k2XJlv1HnDng4o3PZRScm84vaFeERqHHp1bh4pDMsZ0K7p\n"
                + "2EhiLOSMhctiNysqY2PjOBaETT229O/x1QJ8UlJafqAl06d2r727R/UQ5YKlWKk6\n"
                + "dG4/Ruc9O+TouiHkiTvV1V13mMX9SC5t1c+30/dd5iJYx5lGFu/HsRxENQO1KcwK\n"
                + "hpIsHfFlac1gj5cEmRXXgfyFsDO/A6R9UIPQYM2yfSN0wDhg32lyLBK2xodR+hp0\n"
                + "DzS04kzF3f9K/vIJlvkCAwEAAaOBpzCBpDAfBgNVHSMEGDAWgBTuH6y5q2Ml3f2O\n"
                + "r0n+vAxDAnpn/jAdBgNVHQ4EFgQU4fz7YKxMCdhc9Rv0iLCss+z13acwDAYDVR0T\n"
                + "BAUwAwEB/zALBgNVHQ8EBAMCAQYwRwYDVR0fBEAwPjA8oDqgOIY2aHR0cDovL2Ny\n"
                + "bC5kb25nZmVuZy1uaXNzYW4uY29tLmNuL0FSTC9ERkxfUFZSb290Q0EuY3JsMA0G\n"
                + "CSqGSIb3DQEBCwUAA4ICAQApFcOYxB/ZwLuWx30347gOLg0VQNm+uaSKV40wX+NF\n"
                + "JE3vRz4jGLGm7TFykpQ1VrlgnTj9awPfr31Z7ml4uPVzb1W14ufIlrblEH73em/q\n"
                + "y6TX9SwSxcmDNmamxt7EdFiXG7PWxiawu8FRfRw5kHvFwBLXnfVVHU33pZ5/oJUH\n"
                + "VDdfhPTO+Jykc4yOOicJ5cg1fPq0Saj456FziUMTlEwwyLE7WBoWVUvPVPcsTBBl\n"
                + "5k67GB9CdgArf4FyJKbts2lk790fAEiGsKEKB4QhJ9LtEGBco264nOwR29HheXfT\n"
                + "nB7BSuGPFGy7ptipVWraeqTQzDWIbSmD/OHSmWnuMelMM/IntKnYEDijk/A3fTQ9\n"
                + "2rXcYH4wpOQb94XTo4vZzrOFnUnCgYAWajZOCweYKH6LYkBHhhHKEBcdNNA7ki0g\n"
                + "VUb601qZojW6Cb4Q432+T0u2PUBxMv2at93QdNG8dP148h+wXX6f1Hi8eeOJjrpK\n"
                + "VZPPRE3YB50DboNgkxiN5VphWSfVo9TH5GAssjVleDwNdh95VIcW6kgX99LAQL3a\n"
                + "XN7sPNGzEKV6lv1d0rnSigeDEhWo98Nxh7EjP2uDf7ml92KwyEp7uJVLL5xLnZnR\n"
                + "bgJkGXTqcN+KzujP/6oXg2hFcTHlEbHwL/6Zvi5tLqKv28Ae+H7lUfVDibRTM2Pf\n"
                + "1A==";   // 根证书
        String clientCertBase64 = "MIIDSjCCAjKgAwIBAgIITFNjizbd+zYwDQYJKoZIhExHTAbBgNVBAMMFERGTF9Q\n"
                + "BhMCQ04xGTAXBgNVBAoMEERGTF9QVl9OSVNTQU5fQ0ExHTAbBgNVBAMMFERGTF9Q\n"
                + "Vl9OSVNTQU4gU3ViIENBMB4XDTI0MDUxNTA5MjkwN1oXDTM5MDYwNjA5MjkwN1ow\n"
                + "SjELMAkGA1UEBhMCQ04xDzANBgNVBAoMBk5JU1NBTjERMA8GA1UECwwIRk9UQV9Q\n"
                + "UkQxFzAVBgNVBAMMDmZvdGFjbGllbnQtcHJkMIIBIjANBgkqhkiG9w0BAQEFAAOC\n"
                + "AQ8AMIIBCgKCAQEA6k1TTY+LtuAA0GhO92OGDoCbHf2+SkXwUjoOGNbYxD+qApdl\n"
                + "N0ilTu8e2XaJ10oy7gk7446Thw82LMzrGubb3hNYKnd+3rhn8mwg47GXMNF30Dib\n"
                + "+fOTBVwOTXFtVFcFiNWs4qs+AXJSfVTza0WDCGgCz47Pe3wBxbBES0gna+WRS6q/\n"
                + "B9MsdQJadC9u98sKBfc4EQcohdZPvTMgEJvJwm0i7McAn1VhgpEpxyK8jR28aZoU\n"
                + "S4n1a4SgzvFmukpOR08NiHu58s4S/C//ORN8MIzHw9+JU6MgDQD+POM5Jomj8IPE\n"
                + "mxNJhJlNN9nncvB2H8TOQmm6XmSPGiCOG/BZHQIDAQABozcwNTARBglghkgBhvhC\n"
                + "AQEEBAMCB4AwCwYDVR0PBAQDAgTwMBMGA1UdJQQMMAoGCCsGAQUFBwMCMA0GCSqG\n"
                + "SIb3DQEBCwUAA4IBAQA/4LiUr01VtwRep8ZynN+l/7584mAb0Gtygd0pUdvuO6nI\n"
                + "setDo+rcPShFDq/bzfrCMs2m634TFK6n0skRiEyKhBDJ8hJF7/Zn7UyNuHy0rY9C\n"
                + "yGSaGFXOzzU2g4kOtho5gF+Ut/glzuSjUtXA5WyqtgcsvVWZunhMjcnVIf+XYdPG\n"
                + "zvgbY2OGeynztWxDRHBUQPnHh/oPM5vyb2O8fZTV6QY8qdQwdxqufymna/AsZKlG\n"
                + "DgH3FWF9XsYwC/Vp207luerKN8w5e/lz3FnF/lK9S32KArWhc+3PqRf1oRL0VMxZ\n"
                + "HvTTq3TG4gGO15uV8UjFM4F89ehG4c+2OYOFmfBn" ; // 客户端证书
        String clientKeyBase64 = "-----BEGIN RSA PRIVATE KEY-----\n"
                + "MIIEowIBAAKCAQEA6k1TTY+LtuAA0GhO92OGDoCbHf2++3rhn8mwg47GXMNF30Dib\n"
                + "N0ilTu8e2XaJ10oy7gk7446Thw82LMzrGubb3hNYKnd+3rhn8mwg47GXMNF30Dib\n"
                + "+fOTBVwOTXFtVFcFiNWs4qs+AXJSfVTza0WDCGgCz47Pe3wBxbBES0gna+WRS6q/\n"
                + "B9MsdQJadC9u98sKBfc4EQcohdZPvTMgEJvJwm0i7McAn1VhgpEpxyK8jR28aZoU\n"
                + "S4n1a4SgzvFmukpOR08NiHu58s4S/C//ORN8MIzHw9+JU6MgDQD+POM5Jomj8IPE\n"
                + "mxNJhJlNN9nncvB2H8TOQmm6XmSPGiCOG/BZHQIDAQABAoIBAD9xpMbxwbFJQq8s\n"
                + "L4XJizlOFbKvTOGrwoFhewO62YN6gbyl5iBsKWcQVNgpwvVrABN4qKDbsJL3yYGk\n"
                + "E117ebnOnycQqGgCoxL/uIAvTHNJpRna2lF6ZSskXDMYLbSSw8rjoSbXm7ikjjWl\n"
                + "1SwM7IhQMcKmNwSG9avkKPoBlbsfL925S23rdWgnbVgGerHfktFOhH/GKhx9NVF2\n"
                + "SwKP52Exi6faKnhZYjwoWqRBw5IvxDVLly+GDCupifcVyYN5Rulzrhi50XPaWPj4\n"
                + "ifNVCT5ajkIBpzvv28g/QPKECsP/OKGglYtjT0E8W0FGc2gdsMVu5hL4/86uO3N+\n"
                + "p5V0ePkCgYEA/R0/HbY/wzgzIK9DH4bQS+OdjQTp8AKwIE14sHBLc0lOALmPMQRs\n"
                + "9JX9/hLbj2jnnMeP55dUr2J9uMaRlMA+Ygor6J2JgqsKy03+F+t5t1oukl+VW691\n"
                + "YUl21rCOyWdh2cvaRgnUOpzvvj2maFPqUM13hI2r/SiSnHQPpejvhmsCgYEA7Pks\n"
                + "Kz2RocrODMwq2aKhD+MH2Q+2BTRsTCJTOibkqiB56tBRQN5zmNfDsAI6qEB3d3h/\n"
                + "EwKNGif9KPCTSGTuJHXDrk2XRfERFK5CetoX9RYEeKpfG0wxyddf5geRyQqwZItp\n"
                + "8kM4J0bSB7n7V3EtbcfKTxMDOzWs3yYi+z1cMJcCgYAozb8wBgJPcU7ktPxIJ86K\n"
                + "fV4XrrQbygMkofiT0PaI1kdp3IAV8dWytD9m5xlYDBxSbOVo+z9eqq0DzZcIeNlV\n"
                + "qrgbXtIdK3+o+KC+sGpFwZcCZ8BHMIjmrxYmU/4gv7bJipBcfBcnf8cyTvP1m5mJ\n"
                + "fyYGS2RvgkvoRtmC8IM0kwKBgH4BgJr9ghKl5Cqp5Z8T+tCXx/SDhiX+WhehqN+j\n"
                + "DrPn0OwrGQmFP7JmCIfup4IPxXahe/B/yse9u7+pLKeyA28fr41fdLCcHpkAkBkD\n"
                + "HvoTWw1OioPZYCAThcqq+7oLFQBhoVRQqes4nTW+jwdQBvf45HpngbBaOjHt9Wvi\n"
                + "lMLBAoGBAPoGqkoRTouZRCi+Tid5YfX2IKRxYjNSwwLue84b4jDpWTDO8x1kV0Bz\n"
                + "pM+BWUttJzSwT99NqZTyxj5PihnhKtLJYrwk4eVsUB/obylVLoeJlprWJ8cB2Q+s\n"
                + "1H/rchwCrqKuDeQiXtofOqdm41g/lxPzei3eVddnUKgjXz9RiJR7\n"
                + "-----END RSA PRIVATE KEY-----" ; // 客户端私钥 (PKCS#1格式)
        
        try {
            DownloadClientByCert.beginDowanload(0, fileUrl, targetPath, caCertBase64, clientCertBase64, clientKeyBase64);
        } catch (Exception e) {
            e.printStackTrace();
        }
        String hash = "";
        try {
            hash = HashUtils.calculateHash(targetPath, HashAlgorithm.SHA256);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(hash);
        //sha256|c5309a488f4a51cf63162f73fbcf87bc5629ec8842681d4d9249c38478f2ece3
        //sha256|71efb24ba17e22b8cf3af61cf8081922f9457a1c79116c310ec415c2dee3995c
    }

    public static void beginDowanload(int num, String fileUrl, String targetPath, String caCertBase64, String clientCertBase64, String clientKeyBase64) throws Exception {
            num = DownloadClientByCert.execDownload(num, fileUrl, targetPath, caCertBase64, clientCertBase64, clientKeyBase64);
            if(num == 0) return;
            if(num > 0) {
               DownloadClientByCert.beginDowanload(num, fileUrl, targetPath, caCertBase64, clientCertBase64, clientKeyBase64);
            }
    }
}

补充里面引用到的 其他类的 parseKey方法:

/**
     * Parses a Key instance from a PEM representation.
     * <p>
     * When the provided key is encrypted, the provided pass phrase is applied.
     *
     * @param pemString  a PEM representation of a private key (cannot be null or empty)
     * @param passPhrase optional pass phrase (must be present if the private key is encrypted).
     * @return a  Key instance (never null)
     */
    public static Key parseKey(String pemString, String passPhrase) throws IOException {

        if (passPhrase == null) {
            passPhrase = "";
        }
        try (StringReader reader = new StringReader(pemString); //
             PEMParser pemParser = new PEMParser(reader)) {

            final Object object = pemParser.readObject();
            final JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME);

            final KeyPair kp;

            if (object instanceof PEMEncryptedKeyPair) {
                // Encrypted key - we will use provided password
                final PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder().build(passPhrase.toCharArray());
                kp = converter.getKeyPair(((PEMEncryptedKeyPair) object).decryptKeyPair(decProv));
            } else if (object instanceof PKCS8EncryptedPrivateKeyInfo) {
                // Encrypted key - we will use provided password
                try {
                    final PKCS8EncryptedPrivateKeyInfo encryptedInfo = (PKCS8EncryptedPrivateKeyInfo) object;
                    final InputDecryptorProvider provider = new JceOpenSSLPKCS8DecryptorProviderBuilder().build(passPhrase.toCharArray());
                    final PrivateKeyInfo privateKeyInfo = encryptedInfo.decryptPrivateKeyInfo(provider);
                    return converter.getPrivateKey(privateKeyInfo);
                } catch (PKCSException | OperatorCreationException e) {
                    throw new IOException("Unable to decrypt private key.", e);
                }
            } else if (object instanceof PrivateKeyInfo) {
                return converter.getPrivateKey((PrivateKeyInfo) object);
            } else if (object instanceof SubjectPublicKeyInfo) {
                return converter.getPublicKey((SubjectPublicKeyInfo) object);
            } else {
                // Unencrypted key - no password needed
                kp = converter.getKeyPair((PEMKeyPair) object);
            }
            return kp.getPrivate();
        }
    }

注意:文中caCertBase64填去掉首尾之后的根证书内容,clientCertBase64填去掉首尾之后的client证书内容,clientKeyBase64填PKCS#1格式的客户端私钥,文中证书及私钥只为参考格式,内容已被瞎填修改过,可替换成自己的证书及秘钥使用,运行main方法即可运行下载。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容