Skip to content

Commit 3a41369

Browse files
committed
修复小程序登录接口依赖access_token并支持无token请求 #4138
1 parent b73861b commit 3a41369

3 files changed

Lines changed: 185 additions & 24 deletions

File tree

‎weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaService.java‎

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,20 @@
55
import cn.binarywang.wx.miniapp.config.WxMaConfig;
66
import cn.binarywang.wx.miniapp.executor.ApiSignaturePostRequestExecutor;
77
import com.google.gson.JsonObject;
8+
import java.io.IOException;
89
import java.util.Map;
910
import java.util.function.Function;
11+
import me.chanjar.weixin.common.enums.WxType;
1012
import me.chanjar.weixin.common.error.WxErrorException;
13+
import me.chanjar.weixin.common.error.WxRuntimeException;
1114
import me.chanjar.weixin.common.service.WxImgProcService;
1215
import me.chanjar.weixin.common.service.WxOcrService;
1316
import me.chanjar.weixin.common.service.WxService;
1417
import me.chanjar.weixin.common.util.http.MediaUploadRequestExecutor;
1518
import me.chanjar.weixin.common.util.http.RequestExecutor;
1619
import me.chanjar.weixin.common.util.http.RequestHttp;
20+
import me.chanjar.weixin.common.util.http.SimpleGetRequestExecutor;
21+
import me.chanjar.weixin.common.util.http.SimplePostRequestExecutor;
1722

1823
/**
1924
* 微信小程序主服务接口,定义了所有小程序相关的核心操作方法。
@@ -37,7 +42,7 @@ public interface WxMaService extends WxService {
3742
String SET_DYNAMIC_DATA_URL = "https://api.weixin.qq.com/wxa/setdynamicdata";
3843

3944
/**
40-
* 获取登录后的 session 信息。
45+
* 获取登录后的 session 信息,不获取或携带 access_token。
4146
* 登录参数按原始值传入,无需 URL 编码。为防止泄露凭证,失败异常保留错误码和栈帧,
4247
* 不包含原始请求、响应、cause 或 suppressed 异常;其他运行时异常可能转换为 WxRuntimeException。
4348
*
@@ -124,6 +129,54 @@ String getPaidUnionId(String openid, String transactionId, String mchId, String
124129
*/
125130
<T, E> T execute(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException;
126131

132+
/**
133+
* 执行无需自动获取或携带 access_token 的自定义请求。
134+
* 默认实现直接执行请求;SDK 内置实现同时支持 API 主机配置和系统繁忙重试,
135+
* 不因 token 错误刷新配置中的 access_token。
136+
*
137+
* @param executor 请求执行器
138+
* @param uri 接口地址
139+
* @param data 请求数据
140+
* @param <T> 返回类型
141+
* @param <E> 请求数据类型
142+
* @return 接口响应
143+
* @throws WxErrorException 微信接口调用异常
144+
*/
145+
default <T, E> T executeWithoutToken(RequestExecutor<T, E> executor, String uri, E data)
146+
throws WxErrorException {
147+
try {
148+
return executor.execute(uri, data, WxType.MiniApp);
149+
} catch (IOException e) {
150+
throw new WxRuntimeException(e);
151+
}
152+
}
153+
154+
/**
155+
* 发起无需自动获取或携带 access_token 的 GET 请求。
156+
*
157+
* @param url 接口地址
158+
* @param queryParam 已编码的查询参数,可为 null
159+
* @return 接口响应
160+
* @throws WxErrorException 微信接口调用异常
161+
*/
162+
default String getWithoutToken(String url, String queryParam) throws WxErrorException {
163+
return executeWithoutToken(SimpleGetRequestExecutor.create(getRequestHttp()),
164+
url, queryParam);
165+
}
166+
167+
/**
168+
* 发起无需自动获取或携带 access_token 的 POST 请求,不自动启用接口签名。
169+
*
170+
* @param url 接口地址
171+
* @param postData 请求体
172+
* @return 接口响应
173+
* @throws WxErrorException 微信接口调用异常
174+
*/
175+
default String postWithoutToken(String url, String postData) throws WxErrorException {
176+
return executeWithoutToken(SimplePostRequestExecutor.create(getRequestHttp()),
177+
url, postData);
178+
}
179+
127180
/**
128181
* 执行带有签名的微信API请求。
129182
*

‎weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/BaseWxMaServiceImpl.java‎

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ public WxMaJscode2SessionResult jsCode2SessionInfo(String jsCode) throws WxError
220220
params.put("grant_type", "authorization_code");
221221

222222
String result =
223-
get(JSCODE_TO_SESSION_URL, Joiner.on("&").withKeyValueSeparator("=").join(params));
223+
getWithoutToken(JSCODE_TO_SESSION_URL, Joiner.on("&").withKeyValueSeparator("=").join(params));
224224
return WxMaJscode2SessionResult.fromJson(result);
225225
} catch (WxErrorException e) {
226226
throw SensitiveRequestUtils.sanitize(e);
@@ -373,6 +373,12 @@ public <R, T> R execute(RequestExecutor<R, T> executor, String uri, T data)
373373
dataForLog);
374374
}
375375

376+
@Override
377+
public <R, T> R executeWithoutToken(RequestExecutor<R, T> executor, String uri, T data)
378+
throws WxErrorException {
379+
return executeWithRetry(url -> executor.execute(url, data, WxType.MiniApp), uri, null, false);
380+
}
381+
376382
@Override
377383
public WxMaApiResponse execute(
378384
ApiSignaturePostRequestExecutor<?, ?> executor,
@@ -393,10 +399,16 @@ private static interface ExecutorAction<R> {
393399

394400
private <R, T> R executeWithRetry(ExecutorAction<R> executor, String uri, String dataForLog)
395401
throws WxErrorException {
402+
return executeWithRetry(executor, uri, dataForLog, true);
403+
}
404+
405+
private <R> R executeWithRetry(ExecutorAction<R> executor, String uri, String dataForLog,
406+
boolean withToken) throws WxErrorException {
396407
int retryTimes = 0;
397408
do {
398409
try {
399-
return this.executeInternal(executor, uri, dataForLog, false, JSCODE_TO_SESSION_URL.equals(uri));
410+
return this.executeInternal(executor, uri, dataForLog, false,
411+
!withToken || JSCODE_TO_SESSION_URL.equals(uri), withToken);
400412
} catch (WxErrorException e) {
401413
if (retryTimes + 1 > this.maxRetryTimes) {
402414
log.warn("重试达到最大次数【{}】", maxRetryTimes);
@@ -430,21 +442,21 @@ private <R, T> R executeWithRetry(ExecutorAction<R> executor, String uri, String
430442

431443
private <R, T> R executeInternal(
432444
ExecutorAction<R> executor, String uri, String dataForLog, boolean doNotAutoRefreshToken,
433-
boolean code2Session)
445+
boolean code2Session, boolean withToken)
434446
throws WxErrorException {
435447

436-
if (uri.contains("access_token=")) {
448+
if (withToken && uri.contains("access_token=")) {
437449
throw new IllegalArgumentException("uri参数中不允许有access_token: " + uri);
438450
}
439-
String accessToken = getAccessToken(false);
451+
String accessToken = withToken ? getAccessToken(false) : null;
440452

441453
String effectiveApiHostUrl = this.getWxMaConfig().getEffectiveApiHostUrl();
442454
if (!WxMaConfig.DEFAULT_API_HOST_URL.equals(effectiveApiHostUrl)) {
443455
uri = uri.replace(WxMaConfig.DEFAULT_API_HOST_URL, effectiveApiHostUrl);
444456
}
445457

446458
String uriWithAccessToken =
447-
uri + (uri.contains("?") ? "&" : "?") + "access_token=" + accessToken;
459+
withToken ? uri + (uri.contains("?") ? "&" : "?") + "access_token=" + accessToken : uri;
448460
try {
449461
R result = executor.execute(uriWithAccessToken);
450462
if (code2Session) {
@@ -455,7 +467,7 @@ private <R, T> R executeInternal(
455467
return result;
456468
} catch (WxErrorException e) {
457469
WxError error = e.getError();
458-
if (WxConsts.ACCESS_TOKEN_ERROR_CODES.contains(error.getErrorCode())) {
470+
if (withToken && WxConsts.ACCESS_TOKEN_ERROR_CODES.contains(error.getErrorCode())) {
459471
// 强制设置WxMaConfig的access token过期了,这样在下一次请求里就会刷新access token
460472
Lock lock = this.getWxMaConfig().getAccessTokenLock();
461473
lock.lock();
@@ -474,7 +486,7 @@ private <R, T> R executeInternal(
474486
code2Session ? "[redacted]" : error.getErrorMsg());
475487
// 下一次不再自动重试
476488
// 当小程序误调用第三方平台专属接口时,第三方无法使用小程序的access token,如果可以继续自动获取token会导致无限循环重试,直到栈溢出
477-
return this.executeInternal(executor, uri, dataForLog, true, code2Session);
489+
return this.executeInternal(executor, uri, dataForLog, true, code2Session, withToken);
478490
}
479491
}
480492

‎weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/api/impl/WxMaCode2SessionSecurityTest.java‎

Lines changed: 111 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,24 @@ public void allHttpClientsSendEncodedValuesAndPreserveSession() throws Exception
4747
out.write(body);
4848
}
4949
});
50+
server.createContext("/custom", exchange -> {
51+
query.set(exchange.getRequestURI().getRawQuery());
52+
String result = exchange.getRequestMethod();
53+
if ("POST".equals(result)) {
54+
java.io.ByteArrayOutputStream body = new java.io.ByteArrayOutputStream();
55+
byte[] buffer = new byte[128];
56+
int count;
57+
while ((count = exchange.getRequestBody().read(buffer)) != -1) {
58+
body.write(buffer, 0, count);
59+
}
60+
result += ":" + new String(body.toByteArray(), StandardCharsets.UTF_8);
61+
}
62+
byte[] body = ("{\"result\":\"" + result + "\"}").getBytes(StandardCharsets.UTF_8);
63+
exchange.sendResponseHeaders(200, body.length);
64+
try (java.io.OutputStream out = exchange.getResponseBody()) {
65+
out.write(body);
66+
}
67+
});
5068
server.start();
5169
try (LogCapture logs = new LogCapture()) {
5270
BaseWxMaServiceImpl<?, ?>[] services = {new WxMaServiceImpl(), new WxMaServiceHttpComponentsImpl(),
@@ -58,15 +76,22 @@ public void allHttpClientsSendEncodedValuesAndPreserveSession() throws Exception
5876
config.setApiHostUrl("http://127.0.0.1:" + server.getAddress().getPort());
5977
service.setWxMaConfig(config);
6078
try {
79+
config.expireAccessToken();
80+
assertEquals(service.getWithoutToken("https://api.weixin.qq.com/custom", "value=a%2Fb"), "{\"result\":\"GET\"}");
81+
assertEquals(decode(query.get()).get("value"), "a/b");
82+
assertFalse(query.get().contains("access_token="));
83+
assertEquals(service.postWithoutToken("https://api.weixin.qq.com/custom?value=1", "payload"),
84+
"{\"result\":\"POST:payload\"}");
85+
assertEquals(query.get(), "value=1");
6186
response.set(RESULT);
6287
for (String code : new String[]{"NORMAL_CODE", "a b\n", "a+&=#?%2F", "中文\uD83D\uDE00", ""}) {
6388
assertEquals(service.jsCode2SessionInfo(code).getSessionKey(), SESSION);
6489
Map<String, String> params = decode(query.get());
65-
assertEquals(params.size(), 5);
90+
assertEquals(params.size(), 4);
6691
assertEquals(params.get("js_code"), code);
6792
assertEquals(params.get("secret"), config.getSecret());
6893
assertEquals(params.get("appid"), config.getAppid());
69-
assertEquals(params.get("access_token"), TOKEN);
94+
assertNull(params.get("access_token"));
7095
}
7196
response.set("{\"errcode\":40029,\"errmsg\":\"" + SECRET + TOKEN + "\"}");
7297
try {
@@ -98,10 +123,10 @@ public void errorsAndRetryLogsDoNotContainCredentials() throws Exception {
98123
TestService service = service();
99124
service.getWxMaConfig().setApiHostUrl("http://proxy.invalid");
100125
when(service.executor.execute(anyString(), anyString(), eq(WxType.MiniApp)))
101-
.thenThrow(error(40001)).thenThrow(error(-1)).thenReturn(RESULT);
126+
.thenThrow(error(-1)).thenReturn(RESULT);
102127
assertEquals(service.jsCode2SessionInfo("FAKE_CODE").getSessionKey(), SESSION);
103-
assertEquals(service.tokenRequests, 1);
104-
verify(service.executor, times(3)).execute(startsWith("http://proxy.invalid/"), anyString(), eq(WxType.MiniApp));
128+
assertEquals(service.tokenRequests, 0);
129+
verify(service.executor, times(2)).execute(startsWith("http://proxy.invalid/"), anyString(), eq(WxType.MiniApp));
105130

106131
for (Exception failure : new Exception[]{error(40029), new IOException(SECRET + TOKEN),
107132
new IllegalArgumentException(SECRET + TOKEN), new IllegalStateException(SECRET + TOKEN)}) {
@@ -123,13 +148,13 @@ public void errorsAndRetryLogsDoNotContainCredentials() throws Exception {
123148
}
124149

125150
@Test
126-
public void tokenAndParsingFailuresAreProtectedAndNullRemainsNullFailure() throws Exception {
151+
public void loginDoesNotRequireTokenAndParsingFailuresRemainProtected() throws Exception {
127152
try (LogCapture logs = new LogCapture()) {
128153
TestService service = service();
129154
service.getWxMaConfig().expireAccessToken();
130155
when(service.executor.execute(anyString(), anyString(), eq(WxType.MiniApp))).thenReturn(RESULT);
131156
assertEquals(service.jsCode2SessionInfo("FAKE_CODE").getSessionKey(), SESSION);
132-
assertEquals(service.tokenRequests, 1);
157+
assertEquals(service.tokenRequests, 0);
133158
when(service.executor.execute(anyString(), anyString(), eq(WxType.MiniApp)))
134159
.thenReturn("{\"session_key\":\"" + SESSION + "\",\"expires_in\":");
135160
try {
@@ -146,16 +171,82 @@ public void tokenAndParsingFailuresAreProtectedAndNullRemainsNullFailure() throw
146171
}
147172
service.getWxMaConfig().expireAccessToken();
148173
service.failToken = true;
149-
try {
150-
service.jsCode2SessionInfo("FAKE_CODE");
151-
fail("Expected token failure");
152-
} catch (WxRuntimeException e) {
153-
assertSafe(e);
154-
}
174+
when(service.executor.execute(anyString(), anyString(), eq(WxType.MiniApp))).thenReturn(RESULT);
175+
assertEquals(service.jsCode2SessionInfo("FAKE_CODE").getSessionKey(), SESSION);
176+
assertEquals(service.tokenRequests, 0);
155177
logs.assertSafe();
156178
}
157179
}
158180

181+
@Test
182+
public void tokenFreeExecutionRetriesBusyButDoesNotRefreshInvalidToken() throws Exception {
183+
TestService service = service();
184+
service.getWxMaConfig().setApiHostUrl("http://proxy.invalid");
185+
when(service.executor.execute(anyString(), eq("payload"), eq(WxType.MiniApp)))
186+
.thenThrow(error(-1)).thenReturn("ok");
187+
assertEquals(service.executeWithoutToken(service.executor,
188+
"https://api.weixin.qq.com/custom", "payload"), "ok");
189+
verify(service.executor, times(2)).execute("http://proxy.invalid/custom", "payload", WxType.MiniApp);
190+
assertEquals(service.tokenRequests, 0);
191+
192+
reset(service.executor);
193+
when(service.executor.execute(anyString(), anyString(), eq(WxType.MiniApp))).thenThrow(error(40001));
194+
try {
195+
service.executeWithoutToken(service.executor, "https://api.weixin.qq.com/custom", "payload");
196+
fail("Expected invalid token error");
197+
} catch (WxErrorException e) {
198+
assertEquals(e.getError().getErrorCode(), 40001);
199+
}
200+
verify(service.executor).execute(anyString(), anyString(), eq(WxType.MiniApp));
201+
assertEquals(service.getWxMaConfig().getAccessToken(), TOKEN);
202+
assertFalse(service.getWxMaConfig().isAccessTokenExpired());
203+
assertEquals(service.tokenRequests, 0);
204+
}
205+
206+
@Test
207+
public void tokenFreeExecutionStopsAtRetryLimit() throws Exception {
208+
TestService service = service();
209+
service.setMaxRetryTimes(2);
210+
when(service.executor.execute(anyString(), anyString(), eq(WxType.MiniApp))).thenThrow(error(-1));
211+
try {
212+
service.executeWithoutToken(service.executor, "https://api.weixin.qq.com/custom", "payload");
213+
fail("Expected exhausted retries");
214+
} catch (WxErrorException e) {
215+
assertEquals(e.getError().getErrorCode(), -1);
216+
}
217+
verify(service.executor, times(3)).execute(anyString(), anyString(), eq(WxType.MiniApp));
218+
assertEquals(service.tokenRequests, 0);
219+
}
220+
221+
@Test
222+
public void customServiceDefaultExecutionPreservesTransportContract() throws Exception {
223+
cn.binarywang.wx.miniapp.api.WxMaService service =
224+
mock(cn.binarywang.wx.miniapp.api.WxMaService.class, CALLS_REAL_METHODS);
225+
RequestExecutor<String, String> executor = mock(RequestExecutor.class);
226+
when(executor.execute("https://example.invalid/custom", "payload", WxType.MiniApp)).thenReturn("ok");
227+
assertEquals(service.executeWithoutToken(executor, "https://example.invalid/custom", "payload"), "ok");
228+
IOException failure = new IOException("transport failure");
229+
when(executor.execute(anyString(), anyString(), eq(WxType.MiniApp))).thenThrow(failure);
230+
try {
231+
service.executeWithoutToken(executor, "https://example.invalid/custom", "payload");
232+
fail("Expected transport failure");
233+
} catch (WxRuntimeException e) {
234+
assertSame(e.getCause(), failure);
235+
}
236+
verify(service, never()).getAccessToken(anyBoolean());
237+
}
238+
239+
@Test
240+
public void ordinaryExecutionStillAddsAndRefreshesToken() throws Exception {
241+
TestService service = service();
242+
when(service.executor.execute(anyString(), anyString(), eq(WxType.MiniApp)))
243+
.thenThrow(error(40001)).thenReturn("ok");
244+
assertEquals(service.get("https://api.weixin.qq.com/custom", "query=1"), "ok");
245+
verify(service.executor, times(2)).execute(
246+
"https://api.weixin.qq.com/custom?access_token=" + TOKEN, "query=1", WxType.MiniApp);
247+
assertEquals(service.tokenRequests, 1);
248+
}
249+
159250
@Test
160251
public void ordinaryGetKeepsSerializedQueryAndExceptionContract() throws Exception {
161252
TestService service = service();
@@ -186,10 +277,10 @@ public void accountSwitchKeepsCredentialsWithTheirAccount() throws Exception {
186277
String uri = invocation.getArgument(0);
187278
if ("SECOND_APP_ID".equals(params.get("appid"))) {
188279
assertEquals(params.get("secret"), "SECOND_SECRET");
189-
assertTrue(uri.endsWith("access_token=SECOND_TOKEN"));
280+
assertFalse(uri.contains("access_token="));
190281
} else {
191282
assertEquals(params.get("secret"), SECRET);
192-
assertTrue(uri.endsWith("access_token=" + TOKEN));
283+
assertFalse(uri.contains("access_token="));
193284
}
194285
return RESULT;
195286
});
@@ -285,6 +376,11 @@ public String get(String url, String query) throws WxErrorException {
285376
return (String) super.execute(executor, url, query);
286377
}
287378

379+
@Override
380+
public String getWithoutToken(String url, String query) throws WxErrorException {
381+
return (String) super.executeWithoutToken(executor, url, query);
382+
}
383+
288384
@Override
289385
protected String doGetAccessTokenRequest() throws IOException {
290386
tokenRequests++;

0 commit comments

Comments
 (0)