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;
|
package me.chanjar.weixin.common.util;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
public class RandomUtils {
|
public class RandomUtils {
|
||||||
|
|
||||||
private static final String RANDOM_STR = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
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) {
|
if (random == null) {
|
||||||
synchronized (RandomUtils.class) {
|
synchronized (RandomUtils.class) {
|
||||||
if (random == null) {
|
if (random == null) {
|
||||||
random = new java.util.Random();
|
random = new SecureRandom();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,7 +21,7 @@ public class RandomUtils {
|
|||||||
|
|
||||||
public static String getRandomStr() {
|
public static String getRandomStr() {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
java.util.Random r = getRandom();
|
SecureRandom r = getRandom();
|
||||||
for (int i = 0; i < 16; i++) {
|
for (int i = 0; i < 16; i++) {
|
||||||
sb.append(RANDOM_STR.charAt(r.nextInt(RANDOM_STR.length())));
|
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.io.StringReader;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.SecureRandom;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <pre>
|
* <pre>
|
||||||
@@ -37,13 +38,13 @@ public class WxCryptUtil {
|
|||||||
private static final Base64 BASE64 = new Base64();
|
private static final Base64 BASE64 = new Base64();
|
||||||
private static final Charset CHARSET = StandardCharsets.UTF_8;
|
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) {
|
if (random == null) {
|
||||||
synchronized (WxCryptUtil.class) {
|
synchronized (WxCryptUtil.class) {
|
||||||
if (random == null) {
|
if (random == null) {
|
||||||
random = new Random();
|
random = new SecureRandom();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,7 +123,7 @@ public class WxCryptUtil {
|
|||||||
*/
|
*/
|
||||||
private static String genRandomStr() {
|
private static String genRandomStr() {
|
||||||
String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
Random r = getRandom();
|
SecureRandom r = getRandom();
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
for (int i = 0; i < 16; i++) {
|
for (int i = 0; i < 16; i++) {
|
||||||
int number = r.nextInt(base.length());
|
int number = r.nextInt(base.length());
|
||||||
@@ -131,6 +132,20 @@ public class WxCryptUtil {
|
|||||||
return sb.toString();
|
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消息.
|
* 生成xml消息.
|
||||||
*
|
*
|
||||||
@@ -296,7 +311,7 @@ public class WxCryptUtil {
|
|||||||
public String decryptContent(String msgSignature, String timeStamp, String nonce, String encryptedContent) {
|
public String decryptContent(String msgSignature, String timeStamp, String nonce, String encryptedContent) {
|
||||||
// 验证安全签名
|
// 验证安全签名
|
||||||
String signature = SHA1.gen(this.token, timeStamp, nonce, encryptedContent);
|
String signature = SHA1.gen(this.token, timeStamp, nonce, encryptedContent);
|
||||||
if (!signature.equals(msgSignature)) {
|
if (!isSignatureEqual(signature, msgSignature)) {
|
||||||
throw new WxRuntimeException("加密消息签名校验失败");
|
throw new WxRuntimeException("加密消息签名校验失败");
|
||||||
}
|
}
|
||||||
// 解密
|
// 解密
|
||||||
|
|||||||
@@ -99,6 +99,14 @@ public class DefaultApacheHttpClientBuilder implements ApacheHttpClientBuilder {
|
|||||||
*/
|
*/
|
||||||
private String[] supportedProtocols = {"TLSv1.2", "TLSv1.3", "TLSv1.1", "TLSv1"};
|
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() {
|
private SSLConnectionSocketFactory buildSSLConnectionSocketFactory() {
|
||||||
try {
|
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(
|
return new SSLConnectionSocketFactory(
|
||||||
sslcontext,
|
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.PoolingHttpClientConnectionManager;
|
||||||
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
|
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
|
||||||
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
|
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.NoopHostnameVerifier;
|
||||||
import org.apache.hc.client5.http.ssl.TrustAllStrategy;
|
import org.apache.hc.client5.http.ssl.TrustAllStrategy;
|
||||||
import org.apache.hc.core5.http.HttpHost;
|
import org.apache.hc.core5.http.HttpHost;
|
||||||
@@ -94,6 +95,14 @@ public class DefaultHttpComponentsClientBuilder implements HttpComponentsClientB
|
|||||||
*/
|
*/
|
||||||
private String userAgent;
|
private String userAgent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否跳过服务器端证书校验,默认false,即校验证书。
|
||||||
|
* <p>
|
||||||
|
* 仅在自签名证书的抓包代理等特殊调试场景下才可以设置为true,生产环境开启会导致中间人攻击风险。
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
private boolean skipServerCertificateVerification = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 自定义请求拦截器
|
* 自定义请求拦截器
|
||||||
*/
|
*/
|
||||||
@@ -173,16 +182,21 @@ public class DefaultHttpComponentsClientBuilder implements HttpComponentsClientB
|
|||||||
|
|
||||||
SSLContext sslcontext;
|
SSLContext sslcontext;
|
||||||
try {
|
try {
|
||||||
|
if (this.skipServerCertificateVerification) {
|
||||||
sslcontext = SSLContexts.custom()
|
sslcontext = SSLContexts.custom()
|
||||||
.loadTrustMaterial(TrustAllStrategy.INSTANCE) // 忽略对服务器端证书的校验
|
.loadTrustMaterial(TrustAllStrategy.INSTANCE) // 忽略对服务器端证书的校验
|
||||||
.build();
|
.build();
|
||||||
|
} else {
|
||||||
|
sslcontext = SSLContexts.createSystemDefault();
|
||||||
|
}
|
||||||
} catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) {
|
} catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) {
|
||||||
log.error("构建 SSLContext 时发生异常!", e);
|
log.error("构建 SSLContext 时发生异常!", e);
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
PoolingHttpClientConnectionManager connManager = PoolingHttpClientConnectionManagerBuilder.create()
|
PoolingHttpClientConnectionManager connManager = PoolingHttpClientConnectionManagerBuilder.create()
|
||||||
.setTlsSocketStrategy(new DefaultClientTlsStrategy(sslcontext, NoopHostnameVerifier.INSTANCE))
|
.setTlsSocketStrategy(new DefaultClientTlsStrategy(sslcontext,
|
||||||
|
this.skipServerCertificateVerification ? NoopHostnameVerifier.INSTANCE : new DefaultHostnameVerifier()))
|
||||||
.setMaxConnTotal(this.maxTotalConn)
|
.setMaxConnTotal(this.maxTotalConn)
|
||||||
.setMaxConnPerRoute(this.maxConnPerHost)
|
.setMaxConnPerRoute(this.maxConnPerHost)
|
||||||
.setDefaultSocketConfig(SocketConfig.custom()
|
.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(),
|
||||||
|
"特殊调试场景下应允许显式跳过证书校验");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ import me.chanjar.weixin.common.error.WxRuntimeException;
|
|||||||
|
|
||||||
import java.security.*;
|
import java.security.*;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author cloudx
|
* @author cloudx
|
||||||
@@ -33,13 +32,13 @@ public class SignUtils {
|
|||||||
return genRandomStr(32);
|
return genRandomStr(32);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static volatile Random random;
|
private static volatile SecureRandom random;
|
||||||
|
|
||||||
private static Random getRandom() {
|
private static SecureRandom getRandom() {
|
||||||
if (random == null) {
|
if (random == null) {
|
||||||
synchronized (SignUtils.class) {
|
synchronized (SignUtils.class) {
|
||||||
if (random == null) {
|
if (random == null) {
|
||||||
random = new Random();
|
random = new SecureRandom();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,7 +53,7 @@ public class SignUtils {
|
|||||||
*/
|
*/
|
||||||
public static String genRandomStr(int length) {
|
public static String genRandomStr(int length) {
|
||||||
String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
Random r = getRandom();
|
SecureRandom r = getRandom();
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
for (int i = 0; i < length; i++) {
|
for (int i = 0; i < length; i++) {
|
||||||
int number = r.nextInt(base.length());
|
int number = r.nextInt(base.length());
|
||||||
|
|||||||
Reference in New Issue
Block a user