fix(security): 证书校验开关接入连接池并补充真实TLS行为测试
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user