Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Expand All @@ -19,7 +21,7 @@ private static java.util.Random getRandom() {

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())));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
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>
Expand All @@ -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();
}
}
}
Expand Down Expand Up @@ -122,7 +123,7 @@ private static int bytesNetworkOrder2Number(byte[] bytesInNetworkOrder) {
*/
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());
Expand All @@ -131,6 +132,20 @@ private static String genRandomStr() {
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消息.
*
Expand Down Expand Up @@ -296,7 +311,7 @@ public String decrypt(String msgSignature, String timeStamp, String nonce, Strin
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("加密消息签名校验失败");
}
// 解密
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
* 自定义请求拦截器
*/
Expand Down Expand Up @@ -263,9 +271,13 @@ private synchronized void prepare() {

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();
Comment on lines +294 to +296

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 将跳过证书开关接入 Apache 连接池

当调用者把 DefaultApacheHttpClientBuilder.get().setSkipServerCertificateVerification(true) 用于自签名抓包或测试环境时,这里创建的 trust-all SSLConnectionSocketFactory 实际不会参与建连:prepare() 已经用注册表里的 this.sslConnectionSocketFactory 构造了 PoolingHttpClientConnectionManager,随后在 builder 上调用的 setSSLSocketFactory(...) 会被该 connection manager 覆盖。因此 Apache 4 默认客户端仍会按默认 trust store 校验证书,和本次改好的 HC5 客户端行为不一致;应把根据开关生成的 socket factory 注册进 connection manager。

Useful? React with 👍 / 👎.

} else {
sslcontext = SSLContexts.createSystemDefault();
}

return new SSLConnectionSocketFactory(
sslcontext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
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;
Expand Down Expand Up @@ -94,6 +95,14 @@ public class DefaultHttpComponentsClientBuilder implements HttpComponentsClientB
*/
private String userAgent;

/**
* 是否跳过服务器端证书校验,默认false,即校验证书。
* <p>
* 仅在自签名证书的抓包代理等特殊调试场景下才可以设置为true,生产环境开启会导致中间人攻击风险。
* </p>
*/
private boolean skipServerCertificateVerification = false;

/**
* 自定义请求拦截器
*/
Expand Down Expand Up @@ -173,16 +182,21 @@ private synchronized void prepare() {

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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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(),
"特殊调试场景下应允许显式跳过证书校验");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import java.security.*;
import java.util.Base64;
import java.util.Random;

/**
* @author cloudx
Expand Down Expand Up @@ -33,13 +32,13 @@ public static String genRandomStr() {
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();
}
}
}
Expand All @@ -54,7 +53,7 @@ private static Random getRandom() {
*/
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());
Expand Down