共计 4767 个字符,预计需要花费 12 分钟才能阅读完成。
背景
微信退款接口需要使用到证书,我参考微信的官方 Demo 进行,部分代码如下:
char[] password = config.getMchID().toCharArray();
InputStream certStream = config.getCertStream();
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(certStream, password);
上面的代码,在本地调试的时候正常跑过,没有出现任何异常,但是放到测试环境之后便会出现下面的异常,这三种异常都是从 ks.load(certStream, password) 这里抛出来的。定位这个问题花费了一些时间,且让我小小总结一下,供大家遇到相同问题时有个参考。
异常类型 1
java.io.IOException: Short read of DER length
at sun.security.util.DerInputStream.getLength(DerInputStream.java:582)
at sun.security.util.DerValue.init(DerValue.java:391)
at sun.security.util.DerValue.<init>(DerValue.java:332)
at sun.security.util.DerValue.<init>(DerValue.java:345)
at sun.security.pkcs12.PKCS12KeyStore.engineLoad(PKCS12KeyStore.java:1914)
at java.security.KeyStore.load(KeyStore.java:1445)
at com.lingyejun.authenticator.ReadPKCS12File$LoadCertInputStream.run(ReadPKCS12File.java:53)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
异常类型 2
java.io.IOException: DerInputStream.getLength(): lengthTag=7, too big.
at sun.security.util.DerInputStream.getLength(DerInputStream.java:599)
at sun.security.util.DerValue.init(DerValue.java:391)
at sun.security.util.DerValue.<init>(DerValue.java:332)
at sun.security.util.DerValue.<init>(DerValue.java:345)
at sun.security.pkcs12.PKCS12KeyStore.engineLoad(PKCS12KeyStore.java:1914)
at java.security.KeyStore.load(KeyStore.java:1445)
at com.lingyejun.authenticator.ReadPKCS12File$LoadCertInputStream.run(ReadPKCS12File.java:53)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
异常类型 3
java.io.IOException: toDerInputStream rejects tag type 54
at sun.security.util.DerValue.toDerInputStream(DerValue.java:874)
at sun.security.pkcs12.PKCS12KeyStore.engineLoad(PKCS12KeyStore.java:1915)
at java.security.KeyStore.load(KeyStore.java:1445)
at com.lingyejun.authenticator.ReadPKCS12File$LoadCertInputStream.run(ReadPKCS12File.java:53)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
结论:keyStore.load(InputStream stream, char[] password) 中的 InputStream 在尝试加载的过程中,如果有其他的线程正在使用或者进行同样的读加载,那么就会抛出上面的异常。
模拟复现
package com.lingyejun.authenticator;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.io.InputStream;
import java.security.*;
import java.security.cert.CertificateException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 模拟加载 certStream 问题
*
* @Author: lingyejun
* @Date: 2019/6/24
* @Describe:
* @Modified By:
*/
public class ReadPKCS12File {
// 线程个数
private static final int THREAD_POOL_SIZE = 10;
// 初始化线程池
private ExecutorService executorService = new ThreadPoolExecutor(THREAD_POOL_SIZE, THREAD_POOL_SIZE,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
// HTTPS 证书的本地路径
private static final String CERT_LOCAL_PATH = "apiclient_cert.p12";
// HTTPS 证书密码,默认密码等于商户号 MCHID
private static final String CERT_PASSWORD = "1509107311";
private static InputStream certStream = ReadPKCS12File.class.getClassLoader().getResourceAsStream(CERT_LOCAL_PATH);
public static void main(String[] args) {ReadPKCS12File readPKCS12File = new ReadPKCS12File();
for (int threadNo = 0; threadNo < THREAD_POOL_SIZE; threadNo++) {readPKCS12File.executorService.execute(readPKCS12File.new LoadCertInputStream());
}
readPKCS12File.executorService.shutdown();}
public class LoadCertInputStream implements Runnable {
@Override
public void run() {
// 证书
char[] password = CERT_PASSWORD.toCharArray();
InputStream certStream = ReadPKCS12File.certStream;
try {KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(certStream, password);
// 实例化密钥库 & 初始化密钥工厂
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, password);
// 创建 SSLContext
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), null, new SecureRandom());
// 余下代码就不写了,,,System.out.println("初始化 SSL 成功!");
} catch (IOException e) {e.printStackTrace();
} catch (CertificateException e) {e.printStackTrace();
} catch (NoSuchAlgorithmException e) {e.printStackTrace();
} catch (UnrecoverableKeyException e) {e.printStackTrace();
} catch (KeyStoreException e) {e.printStackTrace();
} catch (KeyManagementException e) {e.printStackTrace();
}
}
}
}
知道问题之后,我们只需要将 certStream 由全局唯一更改为方法的局部变量即可
InputStream certStream = ReadPKCS12File.certStream
改为
InputStream certStream = ReadPKCS12File.class.getClassLoader().getResourceAsStream(CERT_LOCAL_PATH)
究其原因
微信的官方 Demo 中的,InputStream certStream = config.getCertStream(),这行代码把我给 ’ 误导 ’ 了,我是在外部读取的 pkcs12 文件输入流且 config 对象是单例的,导致多个线程共同访问这行代码时,certStream 不能被正常加载,故出现了上面的问题。
正文完
发表至: java
2019-06-25