🎨 #4130 【小程序】【开放平台】优化代码,防止 code2Session 凭证通过异常和日志泄露
This commit is contained in:
2
pom.xml
2
pom.xml
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -38,6 +38,8 @@ public interface WxMaService extends WxService {
|
||||
|
||||
/**
|
||||
* 获取登录后的 session 信息。
|
||||
* 登录参数按原始值传入,无需 URL 编码。为防止泄露凭证,失败异常保留错误码和栈帧,
|
||||
* 不包含原始请求、响应、cause 或 suppressed 异常;其他运行时异常可能转换为 WxRuntimeException。
|
||||
*
|
||||
* @param jsCode 登录时获取的 code
|
||||
* @return 登录 session 结果对象
|
||||
|
||||
@@ -211,16 +211,22 @@ public abstract class BaseWxMaServiceImpl<H, P> implements WxMaService, RequestH
|
||||
|
||||
@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");
|
||||
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);
|
||||
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
|
||||
@@ -390,7 +396,7 @@ public abstract class BaseWxMaServiceImpl<H, P> implements WxMaService, RequestH
|
||||
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);
|
||||
@@ -423,7 +429,8 @@ public abstract class BaseWxMaServiceImpl<H, P> implements WxMaService, RequestH
|
||||
}
|
||||
|
||||
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=")) {
|
||||
@@ -440,7 +447,11 @@ public abstract class BaseWxMaServiceImpl<H, P> implements WxMaService, RequestH
|
||||
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();
|
||||
@@ -459,15 +470,18 @@ public abstract class BaseWxMaServiceImpl<H, P> implements WxMaService, RequestH
|
||||
}
|
||||
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 {
|
||||
@@ -477,8 +491,12 @@ public abstract class BaseWxMaServiceImpl<H, P> implements WxMaService, RequestH
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -491,7 +509,7 @@ public abstract class BaseWxMaServiceImpl<H, P> implements WxMaService, RequestH
|
||||
* @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) {
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
package cn.binarywang.wx.miniapp.api.impl;
|
||||
|
||||
import ch.qos.logback.classic.Level;
|
||||
import ch.qos.logback.classic.Logger;
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||
import ch.qos.logback.core.read.ListAppender;
|
||||
import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import me.chanjar.weixin.common.enums.WxType;
|
||||
import me.chanjar.weixin.common.error.WxError;
|
||||
import me.chanjar.weixin.common.error.WxErrorException;
|
||||
import me.chanjar.weixin.common.error.WxRuntimeException;
|
||||
import me.chanjar.weixin.common.util.http.RequestExecutor;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.testng.Assert.*;
|
||||
|
||||
public class WxMaCode2SessionSecurityTest {
|
||||
private static final String SECRET = "FAKE_APP_SECRET";
|
||||
private static final String TOKEN = "FAKE_ACCESS_TOKEN";
|
||||
private static final String SESSION = "FAKE_SESSION_KEY";
|
||||
private static final String RESULT = "{\"openid\":\"FAKE_OPENID\",\"session_key\":\"" + SESSION + "\"}";
|
||||
|
||||
@Test
|
||||
public void allHttpClientsSendEncodedValuesAndPreserveSession() throws Exception {
|
||||
AtomicReference<String> query = new AtomicReference<>();
|
||||
AtomicReference<String> response = new AtomicReference<>(RESULT);
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||
server.createContext("/sns/jscode2session", exchange -> {
|
||||
query.set(exchange.getRequestURI().getRawQuery());
|
||||
byte[] body = response.get().getBytes(StandardCharsets.UTF_8);
|
||||
exchange.sendResponseHeaders(200, body.length);
|
||||
try (java.io.OutputStream out = exchange.getResponseBody()) {
|
||||
out.write(body);
|
||||
}
|
||||
});
|
||||
server.start();
|
||||
try (LogCapture logs = new LogCapture()) {
|
||||
BaseWxMaServiceImpl<?, ?>[] services = {new WxMaServiceImpl(), new WxMaServiceHttpComponentsImpl(),
|
||||
new WxMaServiceOkHttpImpl(), new WxMaServiceJoddHttpImpl()};
|
||||
for (BaseWxMaServiceImpl<?, ?> service : services) {
|
||||
WxMaDefaultConfigImpl config = config();
|
||||
config.setAppid("FAKE_APP_ID+&=中文");
|
||||
config.setSecret(SECRET + "+&=中文\uD83D\uDE00");
|
||||
config.setApiHostUrl("http://127.0.0.1:" + server.getAddress().getPort());
|
||||
service.setWxMaConfig(config);
|
||||
try {
|
||||
response.set(RESULT);
|
||||
for (String code : new String[]{"NORMAL_CODE", "a b\n", "a+&=#?%2F", "中文\uD83D\uDE00", ""}) {
|
||||
assertEquals(service.jsCode2SessionInfo(code).getSessionKey(), SESSION);
|
||||
Map<String, String> params = decode(query.get());
|
||||
assertEquals(params.size(), 5);
|
||||
assertEquals(params.get("js_code"), code);
|
||||
assertEquals(params.get("secret"), config.getSecret());
|
||||
assertEquals(params.get("appid"), config.getAppid());
|
||||
assertEquals(params.get("access_token"), TOKEN);
|
||||
}
|
||||
response.set("{\"errcode\":40029,\"errmsg\":\"" + SECRET + TOKEN + "\"}");
|
||||
try {
|
||||
service.jsCode2SessionInfo("FAKE_CODE");
|
||||
fail("Expected WeChat error");
|
||||
} catch (WxErrorException e) {
|
||||
assertEquals(e.getError().getErrorCode(), 40029);
|
||||
assertSafe(e);
|
||||
}
|
||||
} finally {
|
||||
Object client = service.getRequestHttpClient();
|
||||
if (client instanceof Closeable) {
|
||||
((Closeable) client).close();
|
||||
} else if (client instanceof okhttp3.OkHttpClient) {
|
||||
((okhttp3.OkHttpClient) client).connectionPool().evictAll();
|
||||
((okhttp3.OkHttpClient) client).dispatcher().executorService().shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
logs.assertSafe();
|
||||
} finally {
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void errorsAndRetryLogsDoNotContainCredentials() throws Exception {
|
||||
try (LogCapture logs = new LogCapture()) {
|
||||
TestService service = service();
|
||||
service.getWxMaConfig().setApiHostUrl("http://proxy.invalid");
|
||||
when(service.executor.execute(anyString(), anyString(), eq(WxType.MiniApp)))
|
||||
.thenThrow(error(40001)).thenThrow(error(-1)).thenReturn(RESULT);
|
||||
assertEquals(service.jsCode2SessionInfo("FAKE_CODE").getSessionKey(), SESSION);
|
||||
assertEquals(service.tokenRequests, 1);
|
||||
verify(service.executor, times(3)).execute(startsWith("http://proxy.invalid/"), anyString(), eq(WxType.MiniApp));
|
||||
|
||||
for (Exception failure : new Exception[]{error(40029), new IOException(SECRET + TOKEN),
|
||||
new IllegalArgumentException(SECRET + TOKEN), new IllegalStateException(SECRET + TOKEN)}) {
|
||||
reset(service.executor);
|
||||
when(service.executor.execute(anyString(), anyString(), eq(WxType.MiniApp))).thenThrow(failure);
|
||||
try {
|
||||
service.jsCode2SessionInfo("FAKE_CODE");
|
||||
fail("Expected failure");
|
||||
} catch (WxErrorException e) {
|
||||
assertEquals(e.getError().getErrorCode(), 40029);
|
||||
assertNull(e.getError().getJson());
|
||||
assertSafe(e);
|
||||
} catch (RuntimeException e) {
|
||||
assertSafe(e);
|
||||
}
|
||||
}
|
||||
logs.assertSafe();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tokenAndParsingFailuresAreProtectedAndNullRemainsNullFailure() throws Exception {
|
||||
try (LogCapture logs = new LogCapture()) {
|
||||
TestService service = service();
|
||||
service.getWxMaConfig().expireAccessToken();
|
||||
when(service.executor.execute(anyString(), anyString(), eq(WxType.MiniApp))).thenReturn(RESULT);
|
||||
assertEquals(service.jsCode2SessionInfo("FAKE_CODE").getSessionKey(), SESSION);
|
||||
assertEquals(service.tokenRequests, 1);
|
||||
when(service.executor.execute(anyString(), anyString(), eq(WxType.MiniApp)))
|
||||
.thenReturn("{\"session_key\":\"" + SESSION + "\",\"expires_in\":");
|
||||
try {
|
||||
service.jsCode2SessionInfo("FAKE_CODE");
|
||||
fail("Expected malformed response failure");
|
||||
} catch (WxRuntimeException e) {
|
||||
assertSafe(e);
|
||||
}
|
||||
try {
|
||||
service.jsCode2SessionInfo(null);
|
||||
fail("Expected null input failure");
|
||||
} catch (NullPointerException e) {
|
||||
assertSafe(e);
|
||||
}
|
||||
service.getWxMaConfig().expireAccessToken();
|
||||
service.failToken = true;
|
||||
try {
|
||||
service.jsCode2SessionInfo("FAKE_CODE");
|
||||
fail("Expected token failure");
|
||||
} catch (WxRuntimeException e) {
|
||||
assertSafe(e);
|
||||
}
|
||||
logs.assertSafe();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ordinaryGetKeepsSerializedQueryAndExceptionContract() throws Exception {
|
||||
TestService service = service();
|
||||
IllegalArgumentException original = new IllegalArgumentException("ordinary failure");
|
||||
when(service.executor.execute(anyString(), eq("value=a%2Fb"), eq(WxType.MiniApp))).thenThrow(original);
|
||||
try {
|
||||
service.get("https://api.weixin.qq.com/ordinary", "value=a%2Fb");
|
||||
fail("Expected failure");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertSame(e, original);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void accountSwitchKeepsCredentialsWithTheirAccount() throws Exception {
|
||||
TestService service = service();
|
||||
WxMaDefaultConfigImpl first = config();
|
||||
WxMaDefaultConfigImpl second = config();
|
||||
second.setAppid("SECOND_APP_ID");
|
||||
second.setSecret("SECOND_SECRET");
|
||||
second.updateAccessToken("SECOND_TOKEN", 7200);
|
||||
Map<String, cn.binarywang.wx.miniapp.config.WxMaConfig> configs = new HashMap<>();
|
||||
configs.put(first.getAppid(), first);
|
||||
configs.put(second.getAppid(), second);
|
||||
service.setMultiConfigs(configs, first.getAppid());
|
||||
when(service.executor.execute(anyString(), anyString(), eq(WxType.MiniApp))).thenAnswer(invocation -> {
|
||||
Map<String, String> params = decode(invocation.getArgument(1));
|
||||
String uri = invocation.getArgument(0);
|
||||
if ("SECOND_APP_ID".equals(params.get("appid"))) {
|
||||
assertEquals(params.get("secret"), "SECOND_SECRET");
|
||||
assertTrue(uri.endsWith("access_token=SECOND_TOKEN"));
|
||||
} else {
|
||||
assertEquals(params.get("secret"), SECRET);
|
||||
assertTrue(uri.endsWith("access_token=" + TOKEN));
|
||||
}
|
||||
return RESULT;
|
||||
});
|
||||
assertEquals(service.jsCode2SessionInfo("FAKE_CODE").getSessionKey(), SESSION);
|
||||
assertTrue(service.switchover(second.getAppid()));
|
||||
assertEquals(service.jsCode2SessionInfo("FAKE_CODE").getSessionKey(), SESSION);
|
||||
assertTrue(service.switchover(first.getAppid()));
|
||||
assertEquals(service.jsCode2SessionInfo("FAKE_CODE").getSessionKey(), SESSION);
|
||||
}
|
||||
|
||||
private static WxMaDefaultConfigImpl config() {
|
||||
WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl();
|
||||
config.setAppid("FAKE_APP_ID");
|
||||
config.setSecret(SECRET);
|
||||
config.updateAccessToken(TOKEN, 7200);
|
||||
return config;
|
||||
}
|
||||
|
||||
private TestService service() {
|
||||
TestService service = new TestService();
|
||||
service.setWxMaConfig(config());
|
||||
service.setRetrySleepMillis(0);
|
||||
return service;
|
||||
}
|
||||
|
||||
private WxErrorException error(int code) {
|
||||
return new WxErrorException(WxError.builder().errorCode(code).errorMsg(SECRET + TOKEN)
|
||||
.errorMsgEn(SECRET).json(SESSION).build(), new RuntimeException(SECRET));
|
||||
}
|
||||
|
||||
private static Map<String, String> decode(String query) throws Exception {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
for (String pair : query.split("&")) {
|
||||
String[] parts = pair.split("=", 2);
|
||||
result.put(parts[0], URLDecoder.decode(parts[1], "UTF-8"));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void assertSafe(Throwable e) {
|
||||
StringWriter trace = new StringWriter();
|
||||
e.printStackTrace(new PrintWriter(trace));
|
||||
assertNoSecrets(trace.toString());
|
||||
assertNull(e.getCause());
|
||||
assertEquals(e.getSuppressed().length, 0);
|
||||
}
|
||||
|
||||
private static void assertNoSecrets(String text) {
|
||||
for (String secret : new String[]{SECRET, TOKEN, SESSION, "FAKE_CODE", "FAKE_OPENID"}) {
|
||||
assertFalse(text.contains(secret), "Credentials must not appear in diagnostics");
|
||||
}
|
||||
}
|
||||
|
||||
private static class LogCapture implements AutoCloseable {
|
||||
private final Logger logger = (Logger) LoggerFactory.getLogger(BaseWxMaServiceImpl.class);
|
||||
private final Level previous = logger.getLevel();
|
||||
private final ListAppender<ILoggingEvent> appender = new ListAppender<>();
|
||||
|
||||
LogCapture() {
|
||||
logger.setLevel(Level.DEBUG);
|
||||
appender.start();
|
||||
logger.addAppender(appender);
|
||||
}
|
||||
|
||||
void assertSafe() {
|
||||
assertFalse(appender.list.isEmpty());
|
||||
for (ILoggingEvent event : appender.list) {
|
||||
assertNoSecrets(event.getFormattedMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
logger.detachAppender(appender);
|
||||
logger.setLevel(previous);
|
||||
appender.stop();
|
||||
}
|
||||
}
|
||||
|
||||
private static class TestService extends WxMaServiceImpl {
|
||||
@SuppressWarnings("unchecked")
|
||||
private final RequestExecutor<String, String> executor = mock(RequestExecutor.class);
|
||||
private int tokenRequests;
|
||||
private boolean failToken;
|
||||
|
||||
@Override
|
||||
public void initHttp() {
|
||||
// The inherited execution/retry pipeline is exercised with a controlled transport.
|
||||
}
|
||||
|
||||
@Override
|
||||
public String get(String url, String query) throws WxErrorException {
|
||||
return (String) super.execute(executor, url, query);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String doGetAccessTokenRequest() throws IOException {
|
||||
tokenRequests++;
|
||||
if (failToken) {
|
||||
throw new IOException(SECRET + TOKEN);
|
||||
}
|
||||
return "{\"access_token\":\"" + TOKEN + "\",\"expires_in\":7200}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
<suite name="weixin-java-miniapp-unit-tests" verbose="1">
|
||||
<test name="Unit_Test">
|
||||
<classes>
|
||||
<class name="cn.binarywang.wx.miniapp.api.impl.WxMaCode2SessionSecurityTest"/>
|
||||
<class name="cn.binarywang.wx.miniapp.api.impl.WxMaSubscribeServiceImplUrlTest"/>
|
||||
<class name="cn.binarywang.wx.miniapp.api.impl.WxMaUserServiceImplCheckSessionKeyTest"/>
|
||||
</classes>
|
||||
|
||||
@@ -67,6 +67,11 @@
|
||||
<artifactId>testng</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.inject</groupId>
|
||||
<artifactId>guice</artifactId>
|
||||
@@ -94,6 +99,7 @@
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<version>${logback-slf4j1-test.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
|
||||
@@ -496,6 +496,9 @@ public interface WxOpenComponentService {
|
||||
|
||||
/**
|
||||
* Miniapp jscode 2 session wx ma jscode 2 session result.
|
||||
* Pass raw parameter values, without URL encoding. Failures retain error codes and stack
|
||||
* frames but omit raw responses, causes and suppressed exceptions to protect credentials.
|
||||
* Other runtime exception subclasses may be converted to WxRuntimeException.
|
||||
*
|
||||
* @param appId the app id
|
||||
* @param jsCode the js code
|
||||
|
||||
@@ -15,6 +15,7 @@ import me.chanjar.weixin.common.error.WxErrorException;
|
||||
import me.chanjar.weixin.common.error.WxRuntimeException;
|
||||
import me.chanjar.weixin.common.util.crypto.SHA1;
|
||||
import me.chanjar.weixin.common.util.http.URIUtil;
|
||||
import me.chanjar.weixin.common.util.http.SensitiveRequestUtils;
|
||||
import me.chanjar.weixin.common.util.json.GsonParser;
|
||||
import me.chanjar.weixin.common.util.json.WxGsonBuilder;
|
||||
import me.chanjar.weixin.mp.api.WxMpService;
|
||||
@@ -272,7 +273,8 @@ public class WxOpenComponentServiceImpl implements WxOpenComponentService {
|
||||
lock.unlock();
|
||||
}
|
||||
if (this.getWxOpenConfigStorage().autoRefreshToken()) {
|
||||
log.warn("即将重新获取新的access_token,错误代码:{},错误信息:{}", error.getErrorCode(), error.getErrorMsg());
|
||||
log.warn("即将重新获取新的access_token,错误代码:{},错误信息:{}", error.getErrorCode(),
|
||||
WxOpenServiceAbstractImpl.isSensitiveRequest(uri) ? "[redacted]" : error.getErrorMsg());
|
||||
return this.get(uri, accessTokenKey);
|
||||
}
|
||||
}
|
||||
@@ -494,9 +496,18 @@ public class WxOpenComponentServiceImpl implements WxOpenComponentService {
|
||||
|
||||
@Override
|
||||
public WxMaJscode2SessionResult miniappJscode2Session(String appId, String jsCode) throws WxErrorException {
|
||||
String url = String.format(MINIAPP_JSCODE_2_SESSION, appId, jsCode, getWxOpenConfigStorage().getComponentAppId());
|
||||
String responseContent = get(url);
|
||||
return WxMaJscode2SessionResult.fromJson(responseContent);
|
||||
try {
|
||||
String url = String.format(MINIAPP_JSCODE_2_SESSION,
|
||||
SensitiveRequestUtils.encodeQueryValue(String.valueOf(appId)),
|
||||
SensitiveRequestUtils.encodeQueryValue(String.valueOf(jsCode)),
|
||||
SensitiveRequestUtils.encodeQueryValue(String.valueOf(getWxOpenConfigStorage().getComponentAppId())));
|
||||
String responseContent = get(url);
|
||||
return WxMaJscode2SessionResult.fromJson(responseContent);
|
||||
} catch (WxErrorException e) {
|
||||
throw SensitiveRequestUtils.sanitize(e);
|
||||
} catch (RuntimeException e) {
|
||||
throw SensitiveRequestUtils.sanitize(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -43,20 +43,40 @@ public abstract class WxOpenServiceAbstractImpl<H, P> implements WxOpenService,
|
||||
public abstract void initHttp();
|
||||
|
||||
protected <T, E> T execute(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException {
|
||||
boolean sensitive = isSensitiveRequest(uri);
|
||||
try {
|
||||
T result = executor.execute(uri, data, WxType.Open);
|
||||
log.debug("\n【请求地址】: {}\n【请求参数】:{}\n【响应数据】:{}", uri, data, result);
|
||||
if (sensitive) {
|
||||
log.debug("Sensitive request completed");
|
||||
} else {
|
||||
log.debug("\n【请求地址】: {}\n【请求参数】:{}\n【响应数据】:{}", uri, data, result);
|
||||
}
|
||||
return result;
|
||||
} catch (WxErrorException e) {
|
||||
WxError error = e.getError();
|
||||
if (error.getErrorCode() != 0) {
|
||||
log.warn("\n【请求地址】: {}\n【请求参数】:{}\n【错误信息】:{}", uri, data, error);
|
||||
if (sensitive) {
|
||||
log.warn("Sensitive request failed, error code: {}", error.getErrorCode());
|
||||
} else {
|
||||
log.warn("\n【请求地址】: {}\n【请求参数】:{}\n【错误信息】:{}", uri, data, error);
|
||||
}
|
||||
throw new WxErrorException(error, e);
|
||||
}
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
log.warn("\n【请求地址】: {}\n【请求参数】:{}\n【异常信息】:{}", uri, data, e.getMessage());
|
||||
if (sensitive) {
|
||||
log.warn("Sensitive request failed, exception type: {}", e.getClass().getName());
|
||||
} else {
|
||||
log.warn("\n【请求地址】: {}\n【请求参数】:{}\n【异常信息】:{}", uri, data, e.getMessage());
|
||||
}
|
||||
throw new WxRuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
static boolean isSensitiveRequest(String uri) {
|
||||
String loginUrl = WxOpenComponentService.MINIAPP_JSCODE_2_SESSION.split("\\?", 2)[0];
|
||||
String tokenUrl = WxOpenComponentService.API_COMPONENT_TOKEN_URL;
|
||||
return uri.equals(loginUrl) || uri.startsWith(loginUrl + "?")
|
||||
|| uri.equals(tokenUrl) || uri.startsWith(tokenUrl + "?");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
package me.chanjar.weixin.open.api.impl;
|
||||
|
||||
import ch.qos.logback.classic.Level;
|
||||
import ch.qos.logback.classic.Logger;
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||
import ch.qos.logback.core.read.ListAppender;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.net.URI;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import me.chanjar.weixin.common.enums.WxType;
|
||||
import me.chanjar.weixin.common.error.WxError;
|
||||
import me.chanjar.weixin.common.error.WxErrorException;
|
||||
import me.chanjar.weixin.common.error.WxRuntimeException;
|
||||
import me.chanjar.weixin.common.util.http.RequestExecutor;
|
||||
import me.chanjar.weixin.open.api.WxOpenComponentService;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.testng.Assert.*;
|
||||
|
||||
public class WxOpenCode2SessionSecurityTest {
|
||||
private static final String SECRET = "FAKE_COMPONENT_SECRET";
|
||||
private static final String TOKEN = "FAKE_COMPONENT_TOKEN";
|
||||
private static final String SESSION = "FAKE_SESSION_KEY";
|
||||
private static final String RESULT = "{\"openid\":\"FAKE_OPENID\",\"session_key\":\"" + SESSION + "\"}";
|
||||
|
||||
@Test
|
||||
public void encodesParametersAndKeepsGetExtensionAndNullBehavior() throws Exception {
|
||||
try (LogCapture logs = new LogCapture()) {
|
||||
TestService service = service();
|
||||
WxOpenComponentService component = service.getWxOpenComponentService();
|
||||
for (String code : new String[]{"NORMAL_CODE", "a b\n", "a+&=#?%2F", "中文\uD83D\uDE00", "", null}) {
|
||||
reset(service.executor);
|
||||
when(service.executor.execute(anyString(), isNull(), eq(WxType.Open))).thenReturn(RESULT);
|
||||
assertEquals(component.miniappJscode2Session("appid+&=", code).getSessionKey(), SESSION);
|
||||
ArgumentCaptor<String> uri = ArgumentCaptor.forClass(String.class);
|
||||
verify(service.executor).execute(uri.capture(), isNull(), eq(WxType.Open));
|
||||
Map<String, String> query = decode(URI.create(uri.getValue()).getRawQuery());
|
||||
assertEquals(query.size(), 5);
|
||||
assertEquals(query.get("appid"), "appid+&=");
|
||||
assertEquals(query.get("js_code"), String.valueOf(code));
|
||||
assertEquals(query.get("component_appid"), "component+&=");
|
||||
assertEquals(query.get("component_access_token"), TOKEN);
|
||||
}
|
||||
logs.assertSafe();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void refreshesTokenWithoutLoggingLoginOrCredentialPayloads() throws Exception {
|
||||
try (LogCapture logs = new LogCapture()) {
|
||||
TestService service = service();
|
||||
when(service.executor.execute(anyString(), nullable(String.class), eq(WxType.Open)))
|
||||
.thenThrow(error(40001))
|
||||
.thenReturn("{\"component_access_token\":\"" + TOKEN + "\",\"expires_in\":7200}")
|
||||
.thenReturn(RESULT);
|
||||
assertEquals(service.getWxOpenComponentService().miniappJscode2Session("appid", "FAKE_CODE")
|
||||
.getSessionKey(), SESSION);
|
||||
verify(service.executor).execute(eq(WxOpenComponentService.API_COMPONENT_TOKEN_URL),
|
||||
contains(SECRET), eq(WxType.Open));
|
||||
logs.assertSafe();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void protectsWechatTransportAndResponseParsingFailures() throws Exception {
|
||||
try (LogCapture logs = new LogCapture()) {
|
||||
TestService service = service();
|
||||
for (Exception failure : new Exception[]{error(40029), new IOException(SECRET + TOKEN),
|
||||
new IllegalArgumentException(SECRET + TOKEN), new IllegalStateException(SECRET + TOKEN)}) {
|
||||
reset(service.executor);
|
||||
when(service.executor.execute(anyString(), isNull(), eq(WxType.Open))).thenThrow(failure);
|
||||
try {
|
||||
service.getWxOpenComponentService().miniappJscode2Session("appid", "FAKE_CODE");
|
||||
fail("Expected failure");
|
||||
} catch (WxErrorException e) {
|
||||
assertEquals(e.getError().getErrorCode(), 40029);
|
||||
assertNull(e.getError().getJson());
|
||||
assertNull(e.getError().getErrorMsgEn());
|
||||
assertSafe(e);
|
||||
} catch (RuntimeException e) {
|
||||
assertSafe(e);
|
||||
}
|
||||
}
|
||||
reset(service.executor);
|
||||
when(service.executor.execute(anyString(), isNull(), eq(WxType.Open)))
|
||||
.thenReturn("{\"session_key\":\"" + SESSION + "\",\"expires_in\":");
|
||||
try {
|
||||
service.getWxOpenComponentService().miniappJscode2Session("appid", "FAKE_CODE");
|
||||
fail("Expected malformed response failure");
|
||||
} catch (WxRuntimeException e) {
|
||||
assertSafe(e);
|
||||
}
|
||||
logs.assertSafe();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void firstTokenRequestFailureIsProtected() throws Exception {
|
||||
try (LogCapture logs = new LogCapture()) {
|
||||
TestService service = service();
|
||||
service.getWxOpenConfigStorage().expireComponentAccessToken();
|
||||
when(service.executor.execute(anyString(), anyString(), eq(WxType.Open)))
|
||||
.thenThrow(new IOException(SECRET + TOKEN));
|
||||
try {
|
||||
service.getWxOpenComponentService().miniappJscode2Session("appid", "FAKE_CODE");
|
||||
fail("Expected token failure");
|
||||
} catch (WxRuntimeException e) {
|
||||
assertSafe(e);
|
||||
}
|
||||
logs.assertSafe();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sensitiveEndpointMatchingDoesNotChangeOtherRequests() throws Exception {
|
||||
assertTrue(WxOpenServiceAbstractImpl.isSensitiveRequest(WxOpenComponentService.API_COMPONENT_TOKEN_URL));
|
||||
assertTrue(WxOpenServiceAbstractImpl.isSensitiveRequest(
|
||||
"https://api.weixin.qq.com/sns/component/jscode2session?appid=x"));
|
||||
assertFalse(WxOpenServiceAbstractImpl.isSensitiveRequest(
|
||||
"https://api.weixin.qq.com/sns/component/jscode2session-other?appid=x"));
|
||||
TestService service = service();
|
||||
IllegalArgumentException original = new IllegalArgumentException("ordinary failure");
|
||||
when(service.executor.execute(anyString(), eq("value=a%2Fb"), eq(WxType.Open))).thenThrow(original);
|
||||
try {
|
||||
service.get("https://api.weixin.qq.com/ordinary", "value=a%2Fb");
|
||||
fail("Expected failure");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertSame(e, original);
|
||||
}
|
||||
}
|
||||
|
||||
private TestService service() {
|
||||
TestService service = new TestService();
|
||||
WxOpenInMemoryConfigStorage config = new WxOpenInMemoryConfigStorage();
|
||||
config.setComponentAppId("component+&=");
|
||||
config.setComponentAppSecret(SECRET);
|
||||
config.setComponentVerifyTicket("FAKE_TICKET");
|
||||
config.updateComponentAccessToken(TOKEN, 7200);
|
||||
service.setWxOpenConfigStorage(config);
|
||||
return service;
|
||||
}
|
||||
|
||||
private WxErrorException error(int code) {
|
||||
return new WxErrorException(WxError.builder().errorCode(code).errorMsg(SECRET + TOKEN)
|
||||
.errorMsgEn(SECRET).json(SESSION).build(), new RuntimeException(SECRET));
|
||||
}
|
||||
|
||||
private Map<String, String> decode(String query) throws Exception {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
for (String pair : query.split("&")) {
|
||||
String[] parts = pair.split("=", 2);
|
||||
result.put(parts[0], URLDecoder.decode(parts[1], "UTF-8"));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void assertSafe(Throwable e) {
|
||||
StringWriter trace = new StringWriter();
|
||||
e.printStackTrace(new PrintWriter(trace));
|
||||
assertNoSecrets(trace.toString());
|
||||
assertNull(e.getCause());
|
||||
assertEquals(e.getSuppressed().length, 0);
|
||||
}
|
||||
|
||||
private static void assertNoSecrets(String text) {
|
||||
for (String secret : new String[]{SECRET, TOKEN, SESSION, "FAKE_CODE", "FAKE_OPENID", "FAKE_TICKET"}) {
|
||||
assertFalse(text.contains(secret), "Credentials must not appear in diagnostics");
|
||||
}
|
||||
}
|
||||
|
||||
private static class LogCapture implements AutoCloseable {
|
||||
private final Logger[] loggers = {(Logger) LoggerFactory.getLogger(WxOpenServiceAbstractImpl.class),
|
||||
(Logger) LoggerFactory.getLogger(WxOpenComponentServiceImpl.class)};
|
||||
private final Level[] previous = new Level[loggers.length];
|
||||
private final ListAppender<ILoggingEvent> appender = new ListAppender<>();
|
||||
|
||||
LogCapture() {
|
||||
appender.start();
|
||||
for (int i = 0; i < loggers.length; i++) {
|
||||
previous[i] = loggers[i].getLevel();
|
||||
loggers[i].setLevel(Level.DEBUG);
|
||||
loggers[i].addAppender(appender);
|
||||
}
|
||||
}
|
||||
|
||||
void assertSafe() {
|
||||
assertFalse(appender.list.isEmpty());
|
||||
for (ILoggingEvent event : appender.list) {
|
||||
assertNoSecrets(event.getFormattedMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
for (int i = 0; i < loggers.length; i++) {
|
||||
loggers[i].detachAppender(appender);
|
||||
loggers[i].setLevel(previous[i]);
|
||||
}
|
||||
appender.stop();
|
||||
}
|
||||
}
|
||||
|
||||
private static class TestService extends WxOpenServiceImpl {
|
||||
@SuppressWarnings("unchecked")
|
||||
private final RequestExecutor<String, String> executor = mock(RequestExecutor.class);
|
||||
|
||||
@Override
|
||||
public void initHttp() {
|
||||
// Keep the inherited component/token flow with a controlled transport.
|
||||
}
|
||||
|
||||
@Override
|
||||
public String get(String uri, String data) throws WxErrorException {
|
||||
return super.execute(executor, uri, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String post(String uri, String data) throws WxErrorException {
|
||||
return super.execute(executor, uri, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
</test>
|
||||
<test name="Util_Test">
|
||||
<classes>
|
||||
<class name="me.chanjar.weixin.open.api.impl.WxOpenCode2SessionSecurityTest"/>
|
||||
<class name="me.chanjar.weixin.open.util.WxOpenCryptUtilTest"/>
|
||||
<class name="me.chanjar.weixin.open.util.json.WxOpenGsonBuilderTest"/>
|
||||
<class name="me.chanjar.weixin.open.bean.message.WxOpenXmlMessageTest"/>
|
||||
|
||||
Reference in New Issue
Block a user