之前整理过一篇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方法即可运行下载。