@@ -37,13 +38,13 @@ public class WxCryptUtil {
private static final Base64 BASE64 = new Base64();
private static final Charset CHARSET = StandardCharsets.UTF_8;
- private static volatile Random random;
+ private static volatile SecureRandom random;
- private static Random getRandom() {
+ private static SecureRandom getRandom() {
if (random == null) {
synchronized (WxCryptUtil.class) {
if (random == null) {
- random = new Random();
+ random = new SecureRandom();
}
}
}
@@ -122,7 +123,7 @@ private static int bytesNetworkOrder2Number(byte[] bytesInNetworkOrder) {
*/
private static String genRandomStr() {
String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
- Random r = getRandom();
+ SecureRandom r = getRandom();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 16; i++) {
int number = r.nextInt(base.length());
@@ -131,6 +132,20 @@ private static String genRandomStr() {
return sb.toString();
}
+ /**
+ * 以固定时间的方式比较两个签名,避免时序攻击.
+ *
+ * @param expected 本地计算出的签名
+ * @param actual 请求中携带的签名
+ * @return 两者是否一致
+ */
+ private static boolean isSignatureEqual(String expected, String actual) {
+ if (expected == null || actual == null) {
+ return false;
+ }
+ return MessageDigest.isEqual(expected.getBytes(CHARSET), actual.getBytes(CHARSET));
+ }
+
/**
* 生成xml消息.
*
@@ -296,7 +311,7 @@ public String decrypt(String msgSignature, String timeStamp, String nonce, Strin
public String decryptContent(String msgSignature, String timeStamp, String nonce, String encryptedContent) {
// 验证安全签名
String signature = SHA1.gen(this.token, timeStamp, nonce, encryptedContent);
- if (!signature.equals(msgSignature)) {
+ if (!isSignatureEqual(signature, msgSignature)) {
throw new WxRuntimeException("加密消息签名校验失败");
}
// 解密
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 ef7120b768..1ef22e7823 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
@@ -99,6 +99,14 @@ public class DefaultApacheHttpClientBuilder implements ApacheHttpClientBuilder {
*/
private String[] supportedProtocols = {"TLSv1.2", "TLSv1.3", "TLSv1.1", "TLSv1"};
+ /**
+ * 是否跳过服务器端证书校验,默认false,即校验证书。
+ *
+ * 仅在自签名证书的抓包代理等特殊调试场景下才可以设置为true,生产环境开启会导致中间人攻击风险。
+ *
+ */
+ private boolean skipServerCertificateVerification = false;
+
/**
* 自定义请求拦截器
*/
@@ -121,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;
@@ -199,9 +213,10 @@ private synchronized void prepare() {
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);
@@ -221,7 +236,7 @@ private synchronized void prepare() {
HttpClientBuilder httpClientBuilder = HttpClients.custom()
.setConnectionManager(connectionManager)
.setConnectionManagerShared(true)
- .setSSLSocketFactory(this.buildSSLConnectionSocketFactory())
+ .setSSLSocketFactory(httpsSocketFactory)
.setDefaultRequestConfig(RequestConfig.custom()
.setSocketTimeout(this.soTimeout)
.setConnectTimeout(this.connectionTimeout)
@@ -261,11 +276,27 @@ private synchronized void prepare() {
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 = SSLContexts.custom()
+ SSLContext sslcontext;
+ if (this.skipServerCertificateVerification) {
//忽略掉对服务器端证书的校验
- .loadTrustMaterial((TrustStrategy) (chain, authType) -> true).build();
+ sslcontext = SSLContexts.custom().loadTrustMaterial((TrustStrategy) (chain, authType) -> true).build();
+ } else {
+ sslcontext = SSLContexts.createSystemDefault();
+ }
return new SSLConnectionSocketFactory(
sslcontext,
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/DefaultHttpComponentsClientBuilder.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/DefaultHttpComponentsClientBuilder.java
index 4915e31a16..2238ac7d1f 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/DefaultHttpComponentsClientBuilder.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/DefaultHttpComponentsClientBuilder.java
@@ -19,6 +19,7 @@
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
+import org.apache.hc.client5.http.ssl.DefaultHostnameVerifier;
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
import org.apache.hc.client5.http.ssl.TrustAllStrategy;
import org.apache.hc.core5.http.HttpHost;
@@ -94,6 +95,14 @@ public class DefaultHttpComponentsClientBuilder implements HttpComponentsClientB
*/
private String userAgent;
+ /**
+ * 是否跳过服务器端证书校验,默认false,即校验证书。
+ *
+ * 仅在自签名证书的抓包代理等特殊调试场景下才可以设置为true,生产环境开启会导致中间人攻击风险。
+ *
+ */
+ private boolean skipServerCertificateVerification = false;
+
/**
* 自定义请求拦截器
*/
@@ -173,16 +182,21 @@ private synchronized void prepare() {
SSLContext sslcontext;
try {
- sslcontext = SSLContexts.custom()
- .loadTrustMaterial(TrustAllStrategy.INSTANCE) // 忽略对服务器端证书的校验
- .build();
+ if (this.skipServerCertificateVerification) {
+ sslcontext = SSLContexts.custom()
+ .loadTrustMaterial(TrustAllStrategy.INSTANCE) // 忽略对服务器端证书的校验
+ .build();
+ } else {
+ sslcontext = SSLContexts.createSystemDefault();
+ }
} catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) {
log.error("构建 SSLContext 时发生异常!", e);
throw new RuntimeException(e);
}
PoolingHttpClientConnectionManager connManager = PoolingHttpClientConnectionManagerBuilder.create()
- .setTlsSocketStrategy(new DefaultClientTlsStrategy(sslcontext, NoopHostnameVerifier.INSTANCE))
+ .setTlsSocketStrategy(new DefaultClientTlsStrategy(sslcontext,
+ this.skipServerCertificateVerification ? NoopHostnameVerifier.INSTANCE : new DefaultHostnameVerifier()))
.setMaxConnTotal(this.maxTotalConn)
.setMaxConnPerRoute(this.maxConnPerHost)
.setDefaultSocketConfig(SocketConfig.custom()
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
new file mode 100644
index 0000000000..fa161b5071
--- /dev/null
+++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/http/ServerCertificateVerificationTest.java
@@ -0,0 +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 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);
+ return constructor.newInstance();
+ }
+
+ private DefaultHttpComponentsClientBuilder newHttpComponentsBuilder() throws Exception {
+ Constructor constructor =
+ DefaultHttpComponentsClientBuilder.class.getDeclaredConstructor();
+ constructor.setAccessible(true);
+ return constructor.newInstance();
+ }
+
+ private KeyStore createSelfSignedKeyStore() throws Exception {
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
+ keyPairGenerator.initialize(2048);
+ KeyPair keyPair = keyPairGenerator.generateKeyPair();
+
+ 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 9eeba0df4c..5f53aa6352 100644
--- a/weixin-java-common/src/test/resources/testng.xml
+++ b/weixin-java-common/src/test/resources/testng.xml
@@ -4,11 +4,11 @@
-
+
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/SignUtils.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/SignUtils.java
index 37a1ef2efd..5a1b6152d6 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/SignUtils.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/SignUtils.java
@@ -4,7 +4,6 @@
import java.security.*;
import java.util.Base64;
-import java.util.Random;
/**
* @author cloudx
@@ -33,13 +32,13 @@ public static String genRandomStr() {
return genRandomStr(32);
}
- private static volatile Random random;
+ private static volatile SecureRandom random;
- private static Random getRandom() {
+ private static SecureRandom getRandom() {
if (random == null) {
synchronized (SignUtils.class) {
if (random == null) {
- random = new Random();
+ random = new SecureRandom();
}
}
}
@@ -54,7 +53,7 @@ private static Random getRandom() {
*/
public static String genRandomStr(int length) {
String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
- Random r = getRandom();
+ SecureRandom r = getRandom();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
int number = r.nextInt(base.length());