Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@
<jetty.version>9.4.57.v20241219</jetty.version> <!-- 这个不能用10以上的版本,不支持jdk8-->
<bouncycastle.version>1.85</bouncycastle.version>
<spring-data-redis.version>2.3.3.RELEASE</spring-data-redis.version>
<!-- Binding used by log-capture tests against the production SLF4J 1.7 API. -->
<logback-slf4j1-test.version>1.2.13</logback-slf4j1-test.version>
</properties>
<dependencyManagement>
<dependencies>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package me.chanjar.weixin.common.util.http;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import me.chanjar.weixin.common.error.WxError;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.error.WxRuntimeException;

/**
* Safe parameter and exception handling for requests containing credentials.
*/
public final class SensitiveRequestUtils {
private SensitiveRequestUtils() {
}

/**
* Encodes one raw query parameter value, without interpreting existing percent escapes.
*
* @param value raw, non-null parameter value
* @return UTF-8 form-encoded value
* @throws NullPointerException if the value is null
*/
public static String encodeQueryValue(String value) {
try {
return URLEncoder.encode(value, StandardCharsets.UTF_8.name());
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException("UTF-8 is not available");
}
}

/**
* Retains the WeChat error code and stack frames without exposing response data or causes.
* This is intended for credential-bearing entry points, not general exception conversion.
*
* @param failure original failure
* @return safe exception without the original message, JSON, cause or suppressed exceptions
*/
public static WxErrorException sanitize(WxErrorException failure) {
WxErrorException safe = new WxErrorException(new WxError(failure.getError().getErrorCode(),
"Sensitive request failed"));
safe.setStackTrace(failure.getStackTrace());
return safe;
}

/**
* Removes request data from a runtime failure. Common argument, state and null failures
* keep their categories; other runtime failures become {@link WxRuntimeException}.
* Original exception class names and stack frames remain available for diagnosis.
*
* @param failure original failure
* @return safe exception without the original message, cause or suppressed exceptions
*/
public static RuntimeException sanitize(RuntimeException failure) {
String message = "Sensitive request failed (" + failure.getClass().getName() + ")";
RuntimeException safe;
if (failure instanceof IllegalArgumentException) {
safe = new IllegalArgumentException(message);
} else if (failure instanceof IllegalStateException) {
safe = new IllegalStateException(message);
} else if (failure instanceof NullPointerException) {
safe = new NullPointerException(message);
} else {
safe = new WxRuntimeException(message);
}
safe.setStackTrace(failure.getStackTrace());
return safe;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package me.chanjar.weixin.common.util.http;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URLDecoder;
import me.chanjar.weixin.common.error.WxError;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.error.WxRuntimeException;
import org.testng.annotations.Test;

import static org.testng.Assert.*;

public class SensitiveRequestUtilsTest {
@Test
public void encodesRawValuesExactlyOnce() throws Exception {
for (String value : new String[]{"NORMAL_CODE", "", "a b\n", "+&=#?%2F", "中文\uD83D\uDE00"}) {
String encoded = SensitiveRequestUtils.encodeQueryValue(value);
assertEquals(URLDecoder.decode(encoded, "UTF-8"), value);
assertFalse(encoded.contains("&"));
assertFalse(encoded.contains("#"));
}
assertEquals(SensitiveRequestUtils.encodeQueryValue("%2F"), "%252F");
}

@Test(expectedExceptions = NullPointerException.class)
public void doesNotConvertNullToAValue() {
SensitiveRequestUtils.encodeQueryValue(null);
}

@Test
public void removesAllOriginalErrorRepresentations() {
WxError error = WxError.builder().errorCode(40029).errorMsg("FAKE_SECRET")
.errorMsgEn("FAKE_SECRET").json("FAKE_SECRET").build();
WxErrorException original = new WxErrorException(error, new IllegalArgumentException("FAKE_SECRET"));
original.addSuppressed(new IllegalStateException("FAKE_SECRET"));
WxErrorException safe = SensitiveRequestUtils.sanitize(original);
assertEquals(safe.getError().getErrorCode(), 40029);
assertNull(safe.getError().getJson());
assertNull(safe.getError().getErrorMsgEn());
assertSafe(safe, original);
assertEquals(original.getError().getJson(), "FAKE_SECRET");
}

@Test
public void retainsCommonRuntimeCategoriesWithoutCauses() {
for (RuntimeException original : new RuntimeException[]{new IllegalArgumentException("FAKE_SECRET"),
new IllegalStateException("FAKE_SECRET"), new NullPointerException("FAKE_SECRET"),
new WxRuntimeException("FAKE_SECRET")}) {
original.initCause(new RuntimeException("FAKE_SECRET"));
original.addSuppressed(new RuntimeException("FAKE_SECRET"));
RuntimeException safe = SensitiveRequestUtils.sanitize(original);
assertEquals(safe.getClass(), original.getClass());
assertSafe(safe, original);
}
assertTrue(SensitiveRequestUtils.sanitize(new UnsupportedOperationException("FAKE_SECRET"))
instanceof WxRuntimeException);
}

private void assertSafe(Throwable safe, Throwable original) {
StringWriter trace = new StringWriter();
safe.printStackTrace(new PrintWriter(trace));
assertFalse(trace.toString().contains("FAKE_SECRET"));
assertNull(safe.getCause());
assertEquals(safe.getSuppressed().length, 0);
assertEquals(safe.getStackTrace(), original.getStackTrace());
}
}
1 change: 1 addition & 0 deletions weixin-java-common/src/test/resources/testng.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<suite name="Weixin-java-tool-suite" verbose="1">
<test name="Bean_Test">
<classes>
<class name="me.chanjar.weixin.common.util.http.SensitiveRequestUtilsTest"/>
<class name="me.chanjar.weixin.common.bean.WxAccessTokenTest"/>
<class name="me.chanjar.weixin.common.error.WxErrorTest"/>
<class name="me.chanjar.weixin.common.bean.WxMenuTest"/>
Expand Down
1 change: 1 addition & 0 deletions weixin-java-miniapp/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback-slf4j1-test.version}</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ public interface WxMaService extends WxService {

/**
* 获取登录后的 session 信息。
* 登录参数按原始值传入,无需 URL 编码。为防止泄露凭证,失败异常保留错误码和栈帧,
* 不包含原始请求、响应、cause 或 suppressed 异常;其他运行时异常可能转换为 WxRuntimeException。
*
* @param jsCode 登录时获取的 code
* @return 登录 session 结果对象
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,16 +211,22 @@ public String getPaidUnionId(String openid, String transactionId, String mchId,

@Override
public WxMaJscode2SessionResult jsCode2SessionInfo(String jsCode) throws WxErrorException {
final WxMaConfig config = getWxMaConfig();
Map<String, String> params = new HashMap<>(8);
params.put("appid", config.getAppid());
params.put("secret", config.getSecret());
params.put("js_code", jsCode);
params.put("grant_type", "authorization_code");

String result =
get(JSCODE_TO_SESSION_URL, Joiner.on("&").withKeyValueSeparator("=").join(params));
return WxMaJscode2SessionResult.fromJson(result);
try {
final WxMaConfig config = getWxMaConfig();
Map<String, String> params = new HashMap<>(8);
params.put("appid", SensitiveRequestUtils.encodeQueryValue(config.getAppid()));
params.put("secret", SensitiveRequestUtils.encodeQueryValue(config.getSecret()));
params.put("js_code", SensitiveRequestUtils.encodeQueryValue(jsCode));
params.put("grant_type", "authorization_code");

String result =
get(JSCODE_TO_SESSION_URL, Joiner.on("&").withKeyValueSeparator("=").join(params));
return WxMaJscode2SessionResult.fromJson(result);
} catch (WxErrorException e) {
throw SensitiveRequestUtils.sanitize(e);
} catch (RuntimeException e) {
throw SensitiveRequestUtils.sanitize(e);
}
}

@Override
Expand Down Expand Up @@ -390,7 +396,7 @@ private <R, T> R executeWithRetry(ExecutorAction<R> executor, String uri, String
int retryTimes = 0;
do {
try {
return this.executeInternal(executor, uri, dataForLog, false);
return this.executeInternal(executor, uri, dataForLog, false, JSCODE_TO_SESSION_URL.equals(uri));
} catch (WxErrorException e) {
if (retryTimes + 1 > this.maxRetryTimes) {
log.warn("重试达到最大次数【{}】", maxRetryTimes);
Expand Down Expand Up @@ -423,7 +429,8 @@ private <R, T> R executeWithRetry(ExecutorAction<R> executor, String uri, String
}

private <R, T> R executeInternal(
ExecutorAction<R> executor, String uri, String dataForLog, boolean doNotAutoRefreshToken)
ExecutorAction<R> executor, String uri, String dataForLog, boolean doNotAutoRefreshToken,
boolean code2Session)
throws WxErrorException {

if (uri.contains("access_token=")) {
Expand All @@ -440,7 +447,11 @@ private <R, T> R executeInternal(
uri + (uri.contains("?") ? "&" : "?") + "access_token=" + accessToken;
try {
R result = executor.execute(uriWithAccessToken);
log.debug("\n【请求地址】: {}\n【请求参数】:{}\n【响应数据】:{}", uriWithAccessToken, dataForLog, result);
if (code2Session) {
log.debug("code2Session request completed");
} else {
log.debug("\n【请求地址】: {}\n【请求参数】:{}\n【响应数据】:{}", uriWithAccessToken, dataForLog, result);
}
return result;
} catch (WxErrorException e) {
WxError error = e.getError();
Expand All @@ -459,15 +470,18 @@ private <R, T> R executeInternal(
}
if (this.getWxMaConfig().autoRefreshToken() && !doNotAutoRefreshToken) {
log.warn(
"即将重新获取新的access_token,错误代码:{},错误信息:{}", error.getErrorCode(), error.getErrorMsg());
"即将重新获取新的access_token,错误代码:{},错误信息:{}", error.getErrorCode(),
code2Session ? "[redacted]" : error.getErrorMsg());
// 下一次不再自动重试
// 当小程序误调用第三方平台专属接口时,第三方无法使用小程序的access token,如果可以继续自动获取token会导致无限循环重试,直到栈溢出
return this.executeInternal(executor, uri, dataForLog, true);
return this.executeInternal(executor, uri, dataForLog, true, code2Session);
}
}

if (error.getErrorCode() != 0) {
if (error.getErrorCode() == WxMaErrorMsgEnum.CODE_43101.getCode()) {
if (code2Session) {
log.warn("code2Session request failed, error code: {}", error.getErrorCode());
} else if (error.getErrorCode() == WxMaErrorMsgEnum.CODE_43101.getCode()) {
// 43101 日志太多, 打印为debug, 其他情况打印为warn
log.debug("\n【请求地址】: {}\n【请求参数】:{}\n【错误信息】:{}", uriWithAccessToken, dataForLog, error);
} else {
Expand All @@ -477,8 +491,12 @@ private <R, T> R executeInternal(
}
return null;
} catch (IOException e) {
log.warn(
"\n【请求地址】: {}\n【请求参数】:{}\n【异常信息】:{}", uriWithAccessToken, dataForLog, e.getMessage());
if (code2Session) {
log.warn("code2Session request failed, exception type: {}", e.getClass().getName());
} else {
log.warn(
"\n【请求地址】: {}\n【请求参数】:{}\n【异常信息】:{}", uriWithAccessToken, dataForLog, e.getMessage());
}
throw new WxRuntimeException(e);
}
}
Expand All @@ -491,7 +509,7 @@ private <R, T> R executeInternal(
* @throws WxErrorException 异常
*/
protected String extractAccessToken(String resultContent) throws WxErrorException {
log.debug("access-token response: {}", resultContent);
log.debug("access-token response received");
WxMaConfig config = this.getWxMaConfig();
WxError error = WxError.fromJson(resultContent, WxType.MiniApp);
if (error.getErrorCode() != 0) {
Expand Down
Loading
Loading