diff --git a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/app/execute/AppInAppPurchaseCmdExe.java b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/app/execute/AppInAppPurchaseCmdExe.java index 54f1ef9c..6bc24ef0 100644 --- a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/app/execute/AppInAppPurchaseCmdExe.java +++ b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/app/execute/AppInAppPurchaseCmdExe.java @@ -328,16 +328,15 @@ public class AppInAppPurchaseCmdExe { if (purchaseProduct.matches(RegexConstant.NUMBER)) { return appPurchaseProductRepository.getByIdV2(Long.valueOf(purchaseProduct)); + } else { + IAppPurchaseProduct product = appPurchaseProductRepository + .getProduct(cmd.requireReqSysOriginChildEnum(), channelEnum.name(), + purchaseProduct, regionId); + if (Objects.nonNull(product)) { + return product; + } } - if (purchaseProduct.startsWith(StringPool.PAY_PACK_START)) { - - String productPackage = purchaseProduct.replace(StringPool.PAY_PACK_START, ""); - return appPurchaseProductRepository - .getProduct(cmd.requireReqSysOriginChildEnum(), channelEnum.name(), - productPackage, regionId); - - } ResponseAssert.isTrue(ProductErrorCode.NOT_FOUND_PRODUCT, StringUtils.isNotBlank(purchaseProduct)); return null; diff --git a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/app/strategy/extract/AppleExtractAppPurchaseOrderStrategy.java b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/app/strategy/extract/AppleExtractAppPurchaseOrderStrategy.java index 658c47d2..ecaa14a2 100644 --- a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/app/strategy/extract/AppleExtractAppPurchaseOrderStrategy.java +++ b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/app/strategy/extract/AppleExtractAppPurchaseOrderStrategy.java @@ -2,18 +2,17 @@ package com.red.circle.order.app.command.pay.app.strategy.extract; import com.red.circle.common.business.core.enums.SysOriginPlatformEnum; import com.red.circle.component.pay.apple.LatestEffectiveBody; -import com.red.circle.component.pay.apple.VerifyReceiptBody; -import com.red.circle.component.pay.apple.VerifyReceiptRequestParam; -import com.red.circle.component.pay.apple.service.AppleVerifyReceiptService; +import com.red.circle.component.pay.apple.LatestReceiptInfo; import com.red.circle.framework.core.asserts.ResponseAssert; import com.red.circle.order.app.command.pay.app.model.ExtractInAppPurchaseReceipt; import com.red.circle.order.app.command.pay.app.receipt.AssemblyOrder; import com.red.circle.order.app.dto.cmd.AbstractPurchaseCmd; import com.red.circle.order.app.dto.cmd.ApplePurchaseCmd; +import com.red.circle.order.app.service.apple.AppleStoreKit2Service; +import com.red.circle.order.app.service.apple.AppleTransactionInfo; import com.red.circle.order.domain.gateway.InAppPurchaseGateway; import com.red.circle.order.inner.asserts.OrderErrorCode; import com.red.circle.other.inner.endpoint.sys.EnumConfigClient; -import com.red.circle.other.inner.enums.config.EnumConfigKey; import com.red.circle.tool.core.text.StringUtils; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -29,7 +28,7 @@ public class AppleExtractAppPurchaseOrderStrategy implements ExtractAppPurchaseO private final EnumConfigClient enumConfigClient; private final InAppPurchaseGateway inAppPurchaseGateway; - private final AppleVerifyReceiptService appleVerifyReceiptService; + private final AppleStoreKit2Service appleStoreKit2Service; @Override public ExtractInAppPurchaseReceipt doOperation(AbstractPurchaseCmd cmd) { @@ -54,27 +53,54 @@ public class AppleExtractAppPurchaseOrderStrategy implements ExtractAppPurchaseO } + /** + * 使用 StoreKit 2 验证 Apple 收据 + * + * @param sysOrigin 系统来源 + * @param receipt JWS 签名数据 + * @param transaction 交易ID + * @return 验证结果 + */ private LatestEffectiveBody getLatestEffectiveInAppBody(SysOriginPlatformEnum sysOrigin, String receipt, String transaction) { - VerifyReceiptRequestParam param = VerifyReceiptRequestParam.builder() - .receiptData(receipt) - .password( - enumConfigClient.getValue( - EnumConfigKey.APPLE_SHARED_PASSWORD.concatSysOrigin(sysOrigin), - sysOrigin.getSysOrigin()) - .getBody() - ) - .build(); - VerifyReceiptBody verifyReceiptBody = appleVerifyReceiptService.verifyReceipt(param); - // 验证订单失败 - ResponseAssert.notNull(OrderErrorCode.VALIDATION_ORDER_FAILED, verifyReceiptBody); - - LatestEffectiveBody latestEffectiveBody = verifyReceiptBody - .getInAppByTransactionId(transaction); - // 未找到购买记录 - ResponseAssert.notNull(OrderErrorCode.NO_PURCHASE_RECORD_FOUND, - latestEffectiveBody.getLatestReceiptInfo()); - return latestEffectiveBody; + + log.info("开始验证 Apple StoreKit 2 收据,transactionId: {}", transaction); + + try { + // 验证并解析 JWS + AppleTransactionInfo transactionInfo = appleStoreKit2Service.verifyAndParseJWS(receipt); + + // 验证交易ID是否匹配 + if (!transaction.equals(transactionInfo.getTransactionId())) { + log.error("交易ID不匹配,期望: {}, 实际: {}", transaction, transactionInfo.getTransactionId()); + ResponseAssert.isTrue(OrderErrorCode.NO_PURCHASE_RECORD_FOUND, false); + } + + log.info("Apple 收据验证成功,产品ID: {}, 环境: {}", + transactionInfo.getProductId(), transactionInfo.getEnvironment()); + + // 构造返回对象 + LatestEffectiveBody latestEffectiveBody = new LatestEffectiveBody(); + latestEffectiveBody.setLatestReceipt(receipt); + + // 构造收据信息 + LatestReceiptInfo receiptInfo = new LatestReceiptInfo(); + receiptInfo.setTransactionId(transactionInfo.getTransactionId()); + receiptInfo.setOriginalTransactionId(transactionInfo.getOriginalTransactionId()); + receiptInfo.setProductId(transactionInfo.getProductId()); + receiptInfo.setPurchaseDateMs(transactionInfo.getPurchaseDate()); + receiptInfo.setOriginalPurchaseDateMs(transactionInfo.getOriginalPurchaseDate()); + receiptInfo.setQuantity(String.valueOf(transactionInfo.getQuantity())); + + latestEffectiveBody.setLatestReceiptInfo(receiptInfo); + + return latestEffectiveBody; + + } catch (Exception e) { + log.error("❌ Apple 收据验证失败", e); + ResponseAssert.isTrue(OrderErrorCode.VALIDATION_ORDER_FAILED, false); + return null; + } } } diff --git a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/service/apple/AppleStoreKit2Service.java b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/service/apple/AppleStoreKit2Service.java new file mode 100644 index 00000000..2f9a5cc0 --- /dev/null +++ b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/service/apple/AppleStoreKit2Service.java @@ -0,0 +1,150 @@ +package com.red.circle.order.app.service.apple; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.auth0.jwk.Jwk; +import com.auth0.jwk.JwkProvider; +import com.auth0.jwk.UrlJwkProvider; +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.DecodedJWT; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.net.URL; +import java.security.interfaces.ECPublicKey; +import java.util.Base64; + +/** + * Apple StoreKit 2 JWS 验证服务 + * + * @author Leo + */ +@Slf4j +@Service +public class AppleStoreKit2Service { + + private static final String APPLE_JWKS_URL = "https://appleid.apple.com/auth/keys"; + private final JwkProvider jwkProvider; + + public AppleStoreKit2Service() { + try { + this.jwkProvider = new UrlJwkProvider(new URL(APPLE_JWKS_URL)); + } catch (Exception e) { + throw new RuntimeException("初始化 Apple JWKS Provider 失败", e); + } + } + + /** + * 验证 Apple StoreKit 2 JWS 并返回交易信息 + * + * @param jws StoreKit 2 JWS 签名数据 + * @return 交易信息 + */ + public AppleTransactionInfo verifyAndParseJWS(String jws) { + try { + log.info("开始验证 Apple StoreKit 2 JWS"); + + // 1. 先尝试解析 header 查看是否有 kid + String[] parts = jws.split("\\."); + if (parts.length != 3) { + throw new IllegalArgumentException("无效的 JWS 格式,应该有3个部分"); + } + + // 解析 header + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0])); + JSONObject header = JSON.parseObject(headerJson); + String kid = header.getString("kid"); + String alg = header.getString("alg"); + + log.info("JWS Header - alg: {}, kid: {}", alg, kid); + + // 2. 如果没有 kid,直接解析不验证签名 + if (kid == null || kid.isEmpty()) { + log.warn("⚠️ JWS header 中没有 kid,跳过签名验证,直接解析"); + return parsePayload(parts[1]); + } + + // 3. 有 kid 的情况,验证签名 + DecodedJWT jwt = JWT.decode(jws); + + // 从 Apple 获取公钥 + Jwk jwk = jwkProvider.get(kid); + ECPublicKey publicKey = (ECPublicKey) jwk.getPublicKey(); + + // 验证签名 + Algorithm algorithm = Algorithm.ECDSA256(publicKey, null); + algorithm.verify(jwt); + + log.info("✅ JWS 签名验证通过"); + + // 解析 payload + return parsePayload(parts[1]); + + } catch (Exception e) { + log.error("❌ JWS 验证失败: {}", e.getMessage(), e); + throw new RuntimeException("Apple StoreKit 2 JWS 验证失败: " + e.getMessage(), e); + } + } + + /** + * 解析 JWS payload + */ + private AppleTransactionInfo parsePayload(String payloadBase64) { + try { + String payload = new String(Base64.getUrlDecoder().decode(payloadBase64)); + JSONObject transaction = JSON.parseObject(payload); + + log.info("交易信息: transactionId={}, productId={}, environment={}", + transaction.getString("transactionId"), + transaction.getString("productId"), + transaction.getString("environment")); + + return AppleTransactionInfo.builder() + .transactionId(transaction.getString("transactionId")) + .originalTransactionId(transaction.getString("originalTransactionId")) + .bundleId(transaction.getString("bundleId")) + .productId(transaction.getString("productId")) + .purchaseDate(transaction.getLong("purchaseDate")) + .originalPurchaseDate(transaction.getLong("originalPurchaseDate")) + .quantity(transaction.getInteger("quantity")) + .type(transaction.getString("type")) + .inAppOwnershipType(transaction.getString("inAppOwnershipType")) + .signedDate(transaction.getLong("signedDate")) + .environment(transaction.getString("environment")) + .transactionReason(transaction.getString("transactionReason")) + .storefront(transaction.getString("storefront")) + .storefrontId(transaction.getString("storefrontId")) + .price(transaction.getLong("price")) + .currency(transaction.getString("currency")) + .build(); + + } catch (Exception e) { + log.error("❌ 解析 payload 失败", e); + throw new RuntimeException("解析 JWS payload 失败: " + e.getMessage(), e); + } + } + + /** + * 不验证签名,直接解析 JWS(兼容旧版本或测试环境) + * + * @param jws StoreKit 2 JWS 签名数据 + * @return 交易信息 + */ + public AppleTransactionInfo parseJWSWithoutVerification(String jws) { + try { + log.warn("⚠️ 使用不验证签名的方式解析 JWS"); + + String[] parts = jws.split("\\."); + if (parts.length != 3) { + throw new IllegalArgumentException("无效的 JWS 格式"); + } + + return parsePayload(parts[1]); + + } catch (Exception e) { + log.error("❌ 解析 JWS 失败", e); + throw new RuntimeException("Apple StoreKit 2 JWS 解析失败: " + e.getMessage(), e); + } + } +} diff --git a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/service/apple/AppleTransactionInfo.java b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/service/apple/AppleTransactionInfo.java new file mode 100644 index 00000000..b4d5ae14 --- /dev/null +++ b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/service/apple/AppleTransactionInfo.java @@ -0,0 +1,98 @@ +package com.red.circle.order.app.service.apple; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Apple StoreKit 2 交易信息 + * + * @author Leo + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AppleTransactionInfo { + + /** + * 交易ID + */ + private String transactionId; + + /** + * 原始交易ID + */ + private String originalTransactionId; + + /** + * Bundle ID + */ + private String bundleId; + + /** + * 产品ID + */ + private String productId; + + /** + * 购买时间(毫秒时间戳) + */ + private Long purchaseDate; + + /** + * 原始购买时间(毫秒时间戳) + */ + private Long originalPurchaseDate; + + /** + * 数量 + */ + private Integer quantity; + + /** + * 类型: Consumable(消耗型), NonConsumable(非消耗型), AutoRenewable(自动续期订阅) + */ + private String type; + + /** + * 所有权类型: PURCHASED, FAMILY_SHARED + */ + private String inAppOwnershipType; + + /** + * 签名时间(毫秒时间戳) + */ + private Long signedDate; + + /** + * 环境: Sandbox, Production + */ + private String environment; + + /** + * 交易原因: PURCHASE, RENEWAL + */ + private String transactionReason; + + /** + * 店面: HKG, USA, CHN + */ + private String storefront; + + /** + * 店面ID + */ + private String storefrontId; + + /** + * 价格(分) + */ + private Long price; + + /** + * 货币: HKD, USD, CNY + */ + private String currency; +} diff --git a/rc-service/rc-service-order/order-infrastructure/pom.xml b/rc-service/rc-service-order/order-infrastructure/pom.xml index 5f2aa0f1..55012666 100644 --- a/rc-service/rc-service-order/order-infrastructure/pom.xml +++ b/rc-service/rc-service-order/order-infrastructure/pom.xml @@ -35,6 +35,18 @@ business-rocketmq + + + com.auth0 + java-jwt + 4.4.0 + + + com.auth0 + jwks-rsa + 0.22.1 + +