diff --git a/weixin-java-common/pom.xml b/weixin-java-common/pom.xml
index 5916ab6e3..bc79369bc 100644
--- a/weixin-java-common/pom.xml
+++ b/weixin-java-common/pom.xml
@@ -122,6 +122,12 @@
assertj-guava
test
+
+
+ org.bouncycastle
+ bcpkix-jdk18on
+ test
+
org.dom4j
dom4j
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/DefaultApacheHttpClientBuilder.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/DefaultApacheHttpClientBuilder.java
index 59176557d..1ef22e782 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/DefaultApacheHttpClientBuilder.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/DefaultApacheHttpClientBuilder.java
@@ -129,7 +129,13 @@ public class DefaultApacheHttpClientBuilder implements ApacheHttpClientBuilder {
private final HttpRequestRetryHandler defaultHttpRequestRetryHandler = (exception, executionCount, context) -> false;
- private SSLConnectionSocketFactory sslConnectionSocketFactory = SSLConnectionSocketFactory.getSocketFactory();
+ /**
+ * 默认的SSL连接工厂,用于判断使用方是否自定义过连接工厂
+ */
+ private static final SSLConnectionSocketFactory DEFAULT_SSL_CONNECTION_SOCKET_FACTORY =
+ SSLConnectionSocketFactory.getSocketFactory();
+
+ private SSLConnectionSocketFactory sslConnectionSocketFactory = DEFAULT_SSL_CONNECTION_SOCKET_FACTORY;
private final PlainConnectionSocketFactory plainConnectionSocketFactory = PlainConnectionSocketFactory.getSocketFactory();
private String httpProxyHost;
private int httpProxyPort;
@@ -207,9 +213,10 @@ public class DefaultApacheHttpClientBuilder implements ApacheHttpClientBuilder {
if (prepared.get()) {
return;
}
+ SSLConnectionSocketFactory httpsSocketFactory = this.resolveSSLConnectionSocketFactory();
Registry registry = RegistryBuilder.create()
.register("http", this.plainConnectionSocketFactory)
- .register("https", this.sslConnectionSocketFactory)
+ .register("https", httpsSocketFactory)
.build();
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry);
@@ -229,7 +236,7 @@ public class DefaultApacheHttpClientBuilder implements ApacheHttpClientBuilder {
HttpClientBuilder httpClientBuilder = HttpClients.custom()
.setConnectionManager(connectionManager)
.setConnectionManagerShared(true)
- .setSSLSocketFactory(this.buildSSLConnectionSocketFactory())
+ .setSSLSocketFactory(httpsSocketFactory)
.setDefaultRequestConfig(RequestConfig.custom()
.setSocketTimeout(this.soTimeout)
.setConnectTimeout(this.connectionTimeout)
@@ -269,6 +276,18 @@ public class DefaultApacheHttpClientBuilder implements ApacheHttpClientBuilder {
prepared.set(true);
}
+ /**
+ * 使用方自定义的连接工厂优先,否则按照证书校验开关构造连接工厂.
+ */
+ private SSLConnectionSocketFactory resolveSSLConnectionSocketFactory() {
+ if (this.sslConnectionSocketFactory != DEFAULT_SSL_CONNECTION_SOCKET_FACTORY) {
+ return this.sslConnectionSocketFactory;
+ }
+
+ SSLConnectionSocketFactory factory = this.buildSSLConnectionSocketFactory();
+ return factory == null ? DEFAULT_SSL_CONNECTION_SOCKET_FACTORY : factory;
+ }
+
private SSLConnectionSocketFactory buildSSLConnectionSocketFactory() {
try {
SSLContext sslcontext;
diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/http/ServerCertificateVerificationTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/http/ServerCertificateVerificationTest.java
index 8d86785d1..fa161b507 100644
--- a/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/http/ServerCertificateVerificationTest.java
+++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/http/ServerCertificateVerificationTest.java
@@ -1,44 +1,154 @@
package me.chanjar.weixin.common.util.http;
+import com.sun.net.httpserver.HttpsConfigurator;
+import com.sun.net.httpserver.HttpsServer;
import me.chanjar.weixin.common.util.http.apache.DefaultApacheHttpClientBuilder;
import me.chanjar.weixin.common.util.http.hc.DefaultHttpComponentsClientBuilder;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.testng.Assert;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLException;
+import java.io.OutputStream;
import java.lang.reflect.Constructor;
+import java.math.BigInteger;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.Date;
/**
- * 验证默认情况下不会跳过服务器端证书校验,避免中间人攻击.
+ * 验证默认情况下会校验服务器端证书(避免中间人攻击),且显式跳过校验的开关确实生效.
*/
public class ServerCertificateVerificationTest {
+ private static final char[] KEY_STORE_PASSWORD = "wxjava".toCharArray();
+
+ private HttpsServer httpsServer;
+ private String selfSignedUrl;
+
+ @BeforeClass
+ public void startSelfSignedHttpsServer() throws Exception {
+ KeyStore keyStore = createSelfSignedKeyStore();
+ KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ keyManagerFactory.init(keyStore, KEY_STORE_PASSWORD);
+
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(keyManagerFactory.getKeyManagers(), null, null);
+
+ this.httpsServer = HttpsServer.create(new InetSocketAddress(0), 0);
+ this.httpsServer.setHttpsConfigurator(new HttpsConfigurator(sslContext));
+ this.httpsServer.createContext("/", exchange -> {
+ byte[] body = "ok".getBytes(StandardCharsets.UTF_8);
+ exchange.sendResponseHeaders(200, body.length);
+ try (OutputStream out = exchange.getResponseBody()) {
+ out.write(body);
+ }
+ });
+ this.httpsServer.start();
+ this.selfSignedUrl = "https://localhost:" + this.httpsServer.getAddress().getPort() + "/";
+ }
+
+ @AfterClass(alwaysRun = true)
+ public void stopSelfSignedHttpsServer() {
+ if (this.httpsServer != null) {
+ this.httpsServer.stop(0);
+ }
+ }
+
@Test
- public void testApacheBuilderVerifiesCertificateByDefault() throws Exception {
+ public void testApacheBuilderRejectsUntrustedCertificateByDefault() throws Exception {
+ DefaultApacheHttpClientBuilder builder = newApacheBuilder();
+ Assert.assertFalse(builder.isSkipServerCertificateVerification(), "默认应校验服务器端证书");
+
+ try (CloseableHttpClient client = builder.build()) {
+ client.execute(new HttpGet(this.selfSignedUrl));
+ Assert.fail("默认配置下应拒绝自签名证书");
+ } catch (SSLException e) {
+ // 期望的结果:证书校验失败
+ }
+ }
+
+ @Test
+ public void testApacheBuilderAcceptsUntrustedCertificateWhenSkipEnabled() throws Exception {
+ DefaultApacheHttpClientBuilder builder = newApacheBuilder();
+ builder.setSkipServerCertificateVerification(true);
+
+ try (CloseableHttpClient client = builder.build();
+ CloseableHttpResponse response = client.execute(new HttpGet(this.selfSignedUrl))) {
+ Assert.assertEquals(response.getStatusLine().getStatusCode(), 200);
+ }
+ }
+
+ @Test
+ public void testHttpComponentsBuilderRejectsUntrustedCertificateByDefault() throws Exception {
+ DefaultHttpComponentsClientBuilder builder = newHttpComponentsBuilder();
+ Assert.assertFalse(builder.isSkipServerCertificateVerification(), "默认应校验服务器端证书");
+
+ try (org.apache.hc.client5.http.impl.classic.CloseableHttpClient client = builder.build()) {
+ client.execute(new org.apache.hc.client5.http.classic.methods.HttpGet(this.selfSignedUrl));
+ Assert.fail("默认配置下应拒绝自签名证书");
+ } catch (SSLException e) {
+ // 期望的结果:证书校验失败
+ }
+ }
+
+ @Test
+ public void testHttpComponentsBuilderAcceptsUntrustedCertificateWhenSkipEnabled() throws Exception {
+ DefaultHttpComponentsClientBuilder builder = newHttpComponentsBuilder();
+ builder.setSkipServerCertificateVerification(true);
+
+ try (org.apache.hc.client5.http.impl.classic.CloseableHttpClient client = builder.build();
+ org.apache.hc.client5.http.impl.classic.CloseableHttpResponse response =
+ client.execute(new org.apache.hc.client5.http.classic.methods.HttpGet(this.selfSignedUrl))) {
+ Assert.assertEquals(response.getCode(), 200);
+ }
+ }
+
+ private DefaultApacheHttpClientBuilder newApacheBuilder() throws Exception {
Constructor constructor =
DefaultApacheHttpClientBuilder.class.getDeclaredConstructor();
constructor.setAccessible(true);
- DefaultApacheHttpClientBuilder builder = constructor.newInstance();
-
- Assert.assertFalse(builder.isSkipServerCertificateVerification(),
- "默认应校验服务器端证书");
-
- builder.setSkipServerCertificateVerification(true);
- Assert.assertTrue(builder.isSkipServerCertificateVerification(),
- "特殊调试场景下应允许显式跳过证书校验");
+ return constructor.newInstance();
}
- @Test
- public void testHttpComponentsBuilderVerifiesCertificateByDefault() throws Exception {
+ private DefaultHttpComponentsClientBuilder newHttpComponentsBuilder() throws Exception {
Constructor constructor =
DefaultHttpComponentsClientBuilder.class.getDeclaredConstructor();
constructor.setAccessible(true);
- DefaultHttpComponentsClientBuilder builder = constructor.newInstance();
+ return constructor.newInstance();
+ }
- Assert.assertFalse(builder.isSkipServerCertificateVerification(),
- "默认应校验服务器端证书");
+ private KeyStore createSelfSignedKeyStore() throws Exception {
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
+ keyPairGenerator.initialize(2048);
+ KeyPair keyPair = keyPairGenerator.generateKeyPair();
- builder.setSkipServerCertificateVerification(true);
- Assert.assertTrue(builder.isSkipServerCertificateVerification(),
- "特殊调试场景下应允许显式跳过证书校验");
+ long now = System.currentTimeMillis();
+ X500Name subject = new X500Name("CN=localhost");
+ X509Certificate certificate = new JcaX509CertificateConverter().getCertificate(
+ new JcaX509v3CertificateBuilder(subject, BigInteger.valueOf(now),
+ new Date(now - 86400_000L), new Date(now + 86400_000L), subject, keyPair.getPublic())
+ .build(new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate())));
+
+ KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+ keyStore.load(null, null);
+ keyStore.setKeyEntry("wxjava-test", keyPair.getPrivate(), KEY_STORE_PASSWORD,
+ new Certificate[]{certificate});
+ return keyStore;
}
}
diff --git a/weixin-java-common/src/test/resources/testng.xml b/weixin-java-common/src/test/resources/testng.xml
index 9eeba0df4..ec91705e3 100644
--- a/weixin-java-common/src/test/resources/testng.xml
+++ b/weixin-java-common/src/test/resources/testng.xml
@@ -9,6 +9,7 @@
+