1
0
mirror of synced 2026-10-17 21:06:13 +08:00

fix(security): 默认校验服务器证书并使用安全随机数

- 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>
This commit is contained in:
Binary Wang
2026-07-31 03:38:33 +00:00
parent 6014c833bc
commit ec981e9609
6 changed files with 107 additions and 21 deletions

View File

@@ -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())));
}

View File

@@ -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;
/**
* <pre>
@@ -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("加密消息签名校验失败");
}
// 解密

View File

@@ -99,6 +99,14 @@ public class DefaultApacheHttpClientBuilder implements ApacheHttpClientBuilder {
*/
private String[] supportedProtocols = {"TLSv1.2", "TLSv1.3", "TLSv1.1", "TLSv1"};
/**
* 是否跳过服务器端证书校验,默认false,即校验证书。
* <p>
* 仅在自签名证书的抓包代理等特殊调试场景下才可以设置为true,生产环境开启会导致中间人攻击风险。
* </p>
*/
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,

View File

@@ -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,即校验证书。
* <p>
* 仅在自签名证书的抓包代理等特殊调试场景下才可以设置为true,生产环境开启会导致中间人攻击风险。
* </p>
*/
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()

View File

@@ -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<DefaultApacheHttpClientBuilder> 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<DefaultHttpComponentsClientBuilder> constructor =
DefaultHttpComponentsClientBuilder.class.getDeclaredConstructor();
constructor.setAccessible(true);
DefaultHttpComponentsClientBuilder builder = constructor.newInstance();
Assert.assertFalse(builder.isSkipServerCertificateVerification(),
"默认应校验服务器端证书");
builder.setSkipServerCertificateVerification(true);
Assert.assertTrue(builder.isSkipServerCertificateVerification(),
"特殊调试场景下应允许显式跳过证书校验");
}
}