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:
@@ -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())));
|
||||
}
|
||||
|
||||
@@ -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("加密消息签名校验失败");
|
||||
}
|
||||
// 解密
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(),
|
||||
"特殊调试场景下应允许显式跳过证书校验");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user