diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/RandomUtils.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/RandomUtils.java index a9017c0d1..80475c652 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/RandomUtils.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/RandomUtils.java @@ -1,16 +1,18 @@ package me.chanjar.weixin.common.util; +import java.security.SecureRandom; + public class RandomUtils { private static final String RANDOM_STR = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - private static volatile java.util.Random random; + private static volatile SecureRandom random; - private static java.util.Random getRandom() { + private static SecureRandom getRandom() { if (random == null) { synchronized (RandomUtils.class) { if (random == null) { - random = new java.util.Random(); + random = new SecureRandom(); } } } @@ -19,7 +21,7 @@ public class RandomUtils { public static String getRandomStr() { StringBuilder sb = new StringBuilder(); - java.util.Random r = getRandom(); + SecureRandom r = getRandom(); for (int i = 0; i < 16; i++) { sb.append(RANDOM_STR.charAt(r.nextInt(RANDOM_STR.length()))); } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/WxCryptUtil.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/WxCryptUtil.java index 50362636f..8bff9a2b8 100755 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/WxCryptUtil.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/WxCryptUtil.java @@ -18,8 +18,9 @@ import javax.xml.parsers.ParserConfigurationException; import java.io.StringReader; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.SecureRandom; import java.util.Arrays; -import java.util.Random; /** *
@@ -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 @@ public class WxCryptUtil {
*/
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 @@ public class WxCryptUtil {
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 class WxCryptUtil {
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 ef7120b76..59176557d 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;
+
/**
* 自定义请求拦截器
*/
@@ -263,9 +271,13 @@ public class DefaultApacheHttpClientBuilder implements ApacheHttpClientBuilder {
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 4915e31a1..2238ac7d1 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.classic.HttpClients;
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 @@ public class DefaultHttpComponentsClientBuilder implements HttpComponentsClientB
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 000000000..8d86785d1
--- /dev/null
+++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/http/ServerCertificateVerificationTest.java
@@ -0,0 +1,44 @@
+package me.chanjar.weixin.common.util.http;
+
+import me.chanjar.weixin.common.util.http.apache.DefaultApacheHttpClientBuilder;
+import me.chanjar.weixin.common.util.http.hc.DefaultHttpComponentsClientBuilder;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import java.lang.reflect.Constructor;
+
+/**
+ * 验证默认情况下不会跳过服务器端证书校验,避免中间人攻击.
+ */
+public class ServerCertificateVerificationTest {
+
+ @Test
+ public void testApacheBuilderVerifiesCertificateByDefault() 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(),
+ "特殊调试场景下应允许显式跳过证书校验");
+ }
+
+ @Test
+ public void testHttpComponentsBuilderVerifiesCertificateByDefault() throws Exception {
+ Constructor constructor =
+ DefaultHttpComponentsClientBuilder.class.getDeclaredConstructor();
+ constructor.setAccessible(true);
+ DefaultHttpComponentsClientBuilder builder = constructor.newInstance();
+
+ Assert.assertFalse(builder.isSkipServerCertificateVerification(),
+ "默认应校验服务器端证书");
+
+ builder.setSkipServerCertificateVerification(true);
+ Assert.assertTrue(builder.isSkipServerCertificateVerification(),
+ "特殊调试场景下应允许显式跳过证书校验");
+ }
+}
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 37a1ef2ef..5a1b6152d 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 me.chanjar.weixin.common.error.WxRuntimeException;
import java.security.*;
import java.util.Base64;
-import java.util.Random;
/**
* @author cloudx
@@ -33,13 +32,13 @@ public class SignUtils {
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 @@ public class SignUtils {
*/
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());