1
0
mirror of synced 2026-10-11 09:26:12 +08:00

fix(security): 证书校验开关接入连接池并补充真实TLS行为测试

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Binary Wang
2026-07-31 03:52:41 +00:00
parent ec981e9609
commit 3da9f649f6
4 changed files with 157 additions and 21 deletions

View File

@@ -122,6 +122,12 @@
<artifactId>assertj-guava</artifactId>
<scope>test</scope>
</dependency>
<!-- 测试中生成自签名证书,用于验证TLS证书校验行为 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>

View File

@@ -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<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>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;

View File

@@ -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<DefaultApacheHttpClientBuilder> 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<DefaultHttpComponentsClientBuilder> 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;
}
}

View File

@@ -9,6 +9,7 @@
<class name="me.chanjar.weixin.common.util.crypto.WxCryptUtilTest"/>
<class name="me.chanjar.weixin.common.api.WxMessageInMemoryDuplicateCheckerTest"/>
<class name="me.chanjar.weixin.common.session.SessionTest"/>
<class name="me.chanjar.weixin.common.util.http.ServerCertificateVerificationTest"/>
</classes>
</test>
</suite>