From ec981e960909c2de46591c0d96f05bcbf8d91ba9 Mon Sep 17 00:00:00 2001 From: Binary Wang Date: Fri, 31 Jul 2026 03:38:33 +0000 Subject: [PATCH] =?UTF-8?q?fix(security):=20=E9=BB=98=E8=AE=A4=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=E6=9C=8D=E5=8A=A1=E5=99=A8=E8=AF=81=E4=B9=A6=E5=B9=B6?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E5=AE=89=E5=85=A8=E9=9A=8F=E6=9C=BA=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DefaultApacheHttpClientBuilder / DefaultHttpComponentsClientBuilder 默认不再信任所有证书, hc5 实现同时改用 DefaultHostnameVerifier,新增 skipServerCertificateVerification 开关供调试场景使用 - WxCryptUtil、RandomUtils、wxpay v3 SignUtils 改用 SecureRandom 生成随机串/nonce - WxCryptUtil 回调签名比较改为 MessageDigest.isEqual,避免时序攻击 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../weixin/common/util/RandomUtils.java | 10 +++-- .../common/util/crypto/WxCryptUtil.java | 27 +++++++++--- .../DefaultApacheHttpClientBuilder.java | 16 ++++++- .../DefaultHttpComponentsClientBuilder.java | 22 ++++++++-- .../ServerCertificateVerificationTest.java | 44 +++++++++++++++++++ .../binarywang/wxpay/v3/util/SignUtils.java | 9 ++-- 6 files changed, 107 insertions(+), 21 deletions(-) create mode 100644 weixin-java-common/src/test/java/me/chanjar/weixin/common/util/http/ServerCertificateVerificationTest.java 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());