11
This commit is contained in:
1 parent
f7ca3ef396
commit
aec04c1074
4 files changed
+104
-19
No files matched your search
+7
-7
@@ -86,20 +86,20 @@ public class ThirdAggregateOrderPayController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 接收支付宝 OAuth 回调并返回 AlipayJSBridge 收银页。
|
||||
* 接收固定地址的支付宝 OAuth 回调并返回 AlipayJSBridge 收银页。
|
||||
*
|
||||
* <p>租户编码由服务端一次性 state 恢复,确保所有租户共用同一个支付宝授权回调白名单地址。</p>
|
||||
*
|
||||
* @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<String> alipayOauth(@NotBlank @PathVariable String tenantCode,
|
||||
@NotBlank @RequestParam("auth_code") String authCode,
|
||||
public ResponseEntity<String> 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);
|
||||
|
||||
+3
-4
@@ -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 查询当前租户聚合支付状态。
|
||||
|
||||
+79
-8
@@ -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("支付宝授权状态格式错误,请重新扫码");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验聚合会话可继续扫码,并阻止另一平台在首个平台已选定后继续授权。
|
||||
*
|
||||
|
||||
+15
@@ -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。
|
||||
*
|
||||
|
||||
Reference in new issue
Block a user