苹果支付联调 新增StoreKit2 校验
This commit is contained in:
parent
cc5fbde032
commit
421411c99c
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -35,6 +35,18 @@
|
||||
<artifactId>business-rocketmq</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- pom.xml -->
|
||||
<dependency>
|
||||
<groupId>com.auth0</groupId>
|
||||
<artifactId>java-jwt</artifactId>
|
||||
<version>4.4.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.auth0</groupId>
|
||||
<artifactId>jwks-rsa</artifactId>
|
||||
<version>0.22.1</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<parent>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user