diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/controller/appplus/ThirdAggregateOrderPayController.java b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/controller/appplus/ThirdAggregateOrderPayController.java
index 217004474..8dca08bcb 100644
--- a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/controller/appplus/ThirdAggregateOrderPayController.java
+++ b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/controller/appplus/ThirdAggregateOrderPayController.java
@@ -86,20 +86,20 @@ public class ThirdAggregateOrderPayController {
}
/**
- * 接收支付宝 OAuth 回调并返回 AlipayJSBridge 收银页。
+ * 接收固定地址的支付宝 OAuth 回调并返回 AlipayJSBridge 收银页。
+ *
+ *
租户编码由服务端一次性 state 恢复,确保所有租户共用同一个支付宝授权回调白名单地址。
*
- * @param tenantCode 聚合 URL 中的租户唯一编码
* @param authCode 支付宝一次性授权码
- * @param state 服务端创建的一次性 OAuth state
+ * @param state 包含租户编码且由服务端创建的一次性 OAuth state
* @return 带 CSP nonce 的固定支付宝收银页
*/
- @GetMapping(value = "/oauth/alipay/{tenantCode}", produces = MediaType.TEXT_HTML_VALUE)
+ @GetMapping(value = "/oauth/alipay", produces = MediaType.TEXT_HTML_VALUE)
@Inner(value = false)
- public ResponseEntity alipayOauth(@NotBlank @PathVariable String tenantCode,
- @NotBlank @RequestParam("auth_code") String authCode,
+ public ResponseEntity alipayOauth(@NotBlank @RequestParam("auth_code") String authCode,
@NotBlank @RequestParam("state") String state) {
try {
- ThirdAggregateCashierPageData pageData = aggregateOrderPayService.handleAlipayOauth(tenantCode, authCode, state);
+ ThirdAggregateCashierPageData pageData = aggregateOrderPayService.handleAlipayOauth(authCode, state);
return payPageResponse(pageData);
} catch (RuntimeException exception) {
return messageResponse(exception.getMessage(), HttpStatus.BAD_REQUEST);
diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/ThirdAggregateOrderPayService.java b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/ThirdAggregateOrderPayService.java
index 7de1944a5..0139a943f 100644
--- a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/ThirdAggregateOrderPayService.java
+++ b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/ThirdAggregateOrderPayService.java
@@ -57,14 +57,13 @@ public interface ThirdAggregateOrderPayService {
ThirdAggregateCashierPageData handleWechatOauth(String tenantCode, String authCode, String state);
/**
- * 处理支付宝 OAuth 回调并准备 AlipayJSBridge 收银页。
+ * 处理固定地址的支付宝 OAuth 回调,并从一次性 state 恢复租户上下文。
*
- * @param tenantCode 聚合 URL 中的租户唯一编码
* @param authCode 支付宝一次性授权码
- * @param state 服务端创建的一次性 OAuth state
+ * @param state 包含租户编码且由服务端创建的一次性 OAuth state
* @return 支付宝 JSBridge 调起数据和短期状态查询地址
*/
- ThirdAggregateCashierPageData handleAlipayOauth(String tenantCode, String authCode, String state);
+ ThirdAggregateCashierPageData handleAlipayOauth(String authCode, String state);
/**
* 使用短期 ticket 查询当前租户聚合支付状态。
diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/impl/appplus/ThirdAggregateOrderPayServiceImpl.java b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/impl/appplus/ThirdAggregateOrderPayServiceImpl.java
index 38e4525de..aba46850f 100644
--- a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/impl/appplus/ThirdAggregateOrderPayServiceImpl.java
+++ b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/impl/appplus/ThirdAggregateOrderPayServiceImpl.java
@@ -55,6 +55,7 @@ import java.security.SecureRandom;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
+import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
@@ -151,6 +152,16 @@ public class ThirdAggregateOrderPayServiceImpl implements ThirdAggregateOrderPay
*/
private static final String WECHAT_OAUTH_SCOPE = "snsapi_base";
+ /**
+ * 支付宝固定 OAuth 回调路径,避免租户编码进入回调 URL 后要求逐租户配置平台白名单。
+ */
+ private static final String ALIPAY_OAUTH_CALLBACK_PATH = "/aggregate-order-pay/oauth/alipay";
+
+ /**
+ * 支付宝 OAuth state 中租户编码与随机 state 的分隔符。
+ */
+ private static final String OAUTH_STATE_TENANT_SEPARATOR = ".";
+
/**
* 支付宝撤销接口要求稍后重试的标记值。
*/
@@ -292,9 +303,13 @@ public class ThirdAggregateOrderPayServiceImpl implements ThirdAggregateOrderPay
AppPlusPlatformTypeEnum platform = resolveClientPlatform(userAgent);
ThirdAggregatePaySessionEntity session = aggregatePaySessionService.getByAccessTokenHash(sha256(token));
validateCashierSession(session, platform);
- String state = createOauthState(session.getId(), platform);
- String callbackUrl = buildPublicUrl(getPlatformBaseUrl(platform), "/aggregate-order-pay/oauth/"
- + platformPath(platform) + "/" + tenantCode);
+ String storedState = createOauthState(session.getId(), platform);
+ String state = ObjectUtil.equals(platform, AppPlusPlatformTypeEnum.ALI_PAY)
+ ? buildAlipayOauthState(tenantCode, storedState) : storedState;
+ String callbackPath = ObjectUtil.equals(platform, AppPlusPlatformTypeEnum.ALI_PAY)
+ ? ALIPAY_OAUTH_CALLBACK_PATH
+ : "/aggregate-order-pay/oauth/" + platformPath(platform) + "/" + tenantCode;
+ String callbackUrl = buildPublicUrl(getPlatformBaseUrl(platform), callbackPath);
if (ObjectUtil.equals(platform, AppPlusPlatformTypeEnum.WECHAT)) {
return WECHAT_OAUTH_URL + "?appid=" + encode(appPlusPayConfig.getWechatConfig().getGzhAppId())
+ "&redirect_uri=" + encode(callbackUrl)
@@ -329,18 +344,31 @@ public class ThirdAggregateOrderPayServiceImpl implements ThirdAggregateOrderPay
}
/**
- * 消费一次性支付宝 OAuth state,换取付款人身份并创建或复用 JSAPI 子订单。
+ * 处理固定支付宝 OAuth 回调,从一次性 state 中恢复租户编码后进入对应租户上下文。
*
- * @param tenantCode 聚合 URL 中的租户唯一编码
* @param authCode 支付宝一次性授权码
- * @param state 服务端创建的一次性 OAuth state
+ * @param state 包含租户编码和 Redis 一次性随机值的 OAuth state
* @return 支付宝 JSBridge 调起数据和短期状态查询地址
*/
@Override
- public ThirdAggregateCashierPageData handleAlipayOauth(String tenantCode, String authCode, String state) {
+ public ThirdAggregateCashierPageData handleAlipayOauth(String authCode, String state) {
+ String[] stateParts = parseAlipayOauthState(state);
+ return handleAlipayOauthInTenant(stateParts[0], authCode, stateParts[1]);
+ }
+
+ /**
+ * 在指定租户上下文中消费支付宝 OAuth state 并创建或复用 JSAPI 子订单。
+ *
+ * @param tenantCode 已由固定回调 state 确认的租户唯一编码
+ * @param authCode 支付宝一次性授权码
+ * @param storedState Redis 中保存的一次性随机 state
+ * @return 支付宝 JSBridge 调起数据和短期状态查询地址
+ */
+ private ThirdAggregateCashierPageData handleAlipayOauthInTenant(String tenantCode, String authCode,
+ String storedState) {
SysTenant tenant = resolveTenant(tenantCode);
return TenantBroker.applyAs(tenant.getId(), ignored -> {
- ThirdAggregatePaySessionEntity session = consumeOauthState(state, AppPlusPlatformTypeEnum.ALI_PAY);
+ ThirdAggregatePaySessionEntity session = consumeOauthState(storedState, AppPlusPlatformTypeEnum.ALI_PAY);
ThirdAggregateAlipayOauthIdentity identity = aggregateAliPayService.exchangeOauthCode(authCode);
ThirdAggregateCashierLaunch launch = createOrReuseChildOrder(session, AppPlusPlatformTypeEnum.ALI_PAY,
identity.getStableIdentity(), identity);
@@ -766,6 +794,49 @@ public class ThirdAggregateOrderPayServiceImpl implements ThirdAggregateOrderPay
return state;
}
+ /**
+ * 将租户编码绑定到支付宝 OAuth state,租户编码仅用于回调恢复上下文,随机部分仍由 Redis 一次性消费。
+ *
+ * @param tenantCode 聚合二维码中的租户唯一编码
+ * @param storedState 已保存到当前租户 Redis 命名空间的一次性随机 state
+ * @return 可安全放入 OAuth URL 的租户绑定 state
+ */
+ private String buildAlipayOauthState(String tenantCode, String storedState) {
+ if (StrUtil.isBlank(tenantCode) || StrUtil.isBlank(storedState)) {
+ throw new RuntimeException("支付宝授权状态生成失败");
+ }
+ String encodedTenantCode = Base64.getUrlEncoder().withoutPadding()
+ .encodeToString(tenantCode.getBytes(StandardCharsets.UTF_8));
+ return encodedTenantCode + OAUTH_STATE_TENANT_SEPARATOR + storedState;
+ }
+
+ /**
+ * 解析固定支付宝回调携带的租户绑定 state,并拒绝缺段、非法编码或空值。
+ *
+ * @param state 支付宝原样回传的 OAuth state
+ * @return 长度为二的数组,依次为租户唯一编码和 Redis 一次性随机 state
+ */
+ private String[] parseAlipayOauthState(String state) {
+ if (StrUtil.isBlank(state)) {
+ throw new RuntimeException("支付授权状态不能为空");
+ }
+ int separatorIndex = state.indexOf(OAUTH_STATE_TENANT_SEPARATOR);
+ if (separatorIndex <= 0 || separatorIndex >= state.length() - OAUTH_STATE_TENANT_SEPARATOR.length()) {
+ throw new RuntimeException("支付宝授权状态格式错误,请重新扫码");
+ }
+ String encodedTenantCode = state.substring(0, separatorIndex);
+ String storedState = state.substring(separatorIndex + OAUTH_STATE_TENANT_SEPARATOR.length());
+ try {
+ String tenantCode = new String(Base64.getUrlDecoder().decode(encodedTenantCode), StandardCharsets.UTF_8);
+ if (StrUtil.isBlank(tenantCode) || StrUtil.isBlank(storedState)) {
+ throw new RuntimeException("支付宝授权状态格式错误,请重新扫码");
+ }
+ return new String[]{tenantCode, storedState};
+ } catch (IllegalArgumentException exception) {
+ throw new RuntimeException("支付宝授权状态格式错误,请重新扫码");
+ }
+ }
+
/**
* 校验聚合会话可继续扫码,并阻止另一平台在首个平台已选定后继续授权。
*
diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/test/java/com/bsd/pjcloud/thirdparty/service/impl/appplus/ThirdAggregateOrderPayServiceImplTest.java b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/test/java/com/bsd/pjcloud/thirdparty/service/impl/appplus/ThirdAggregateOrderPayServiceImplTest.java
index 8f5273d89..c9e0ec362 100644
--- a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/test/java/com/bsd/pjcloud/thirdparty/service/impl/appplus/ThirdAggregateOrderPayServiceImplTest.java
+++ b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/test/java/com/bsd/pjcloud/thirdparty/service/impl/appplus/ThirdAggregateOrderPayServiceImplTest.java
@@ -207,6 +207,21 @@ class ThirdAggregateOrderPayServiceImplTest {
verify(aggregatePaySessionService, never()).save(any(ThirdAggregatePaySessionEntity.class));
}
+ /**
+ * 验证支付宝扫码授权统一使用不含租户编码的固定回调地址,租户信息由一次性 state 承载。
+ */
+ @Test
+ void alipayOauthShouldUseFixedCallbackUrl() {
+ ThirdAggregatePaySessionEntity session = reusableSession();
+ when(aggregatePaySessionService.getByAccessTokenHash(anyString())).thenReturn(session);
+
+ String oauthUrl = service.getCashierOauthUrl("tenant-code", "cashier-access-token", "AlipayClient/10.6.80");
+
+ Assertions.assertTrue(oauthUrl.contains("redirect_uri=https%3A%2F%2Fpay.example.com%2Fthird%2Faggregate-order-pay%2Foauth%2Falipay"));
+ Assertions.assertFalse(oauthUrl.contains("oauth%2Falipay%2Ftenant-code"));
+ Assertions.assertTrue(oauthUrl.contains("state=dGVuYW50LWNvZGU."));
+ }
+
/**
* 构建支付宝和微信全局应用配置,公网地址固定使用 HTTPS。
*