refactor(service): 改用JWS验签,移除旧收据解析

废弃ReceiptUtility与AppStoreServerAPIClient,直接以SignedDataVerifier校验客户端传来的signedPayload(JWS),简化流程并减少一次网络IO。
This commit is contained in:
2025-12-16 15:06:41 +08:00
parent c305dfaae4
commit c54c14de58
3 changed files with 33 additions and 72 deletions

View File

@@ -62,12 +62,12 @@ public class AppleReceiptController {
if (body == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "body 不能为空");
}
String receipt = body.get("receipt");
if (receipt == null || receipt.isBlank()) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "receipt 不能为空");
String signedPayload = body.get("signedPayload");
if (signedPayload == null || signedPayload.isBlank()) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "signedPayload 不能为空");
}
Long userId = StpUtil.getLoginIdAsLong();
AppleReceiptValidationResult validationResult = appleReceiptService.validateReceipt(receipt);
AppleReceiptValidationResult validationResult = appleReceiptService.validateReceipt(signedPayload);
applePurchaseService.processPurchase(userId, validationResult);
return ResultUtils.success(Boolean.TRUE);
}

View File

@@ -7,7 +7,7 @@ public interface AppleReceiptService {
/**
* 验证 base64 app receipt 是否有效,并返回解析结果。
*
* @param appReceipt Base64 的 app receipt以 MI... 开头那串)
* @param signedPayload Base64 的 app receipt以 MI... 开头那串)
*/
AppleReceiptValidationResult validateReceipt(String appReceipt);
AppleReceiptValidationResult validateReceipt(String signedPayload);
}

View File

@@ -1,11 +1,7 @@
package com.yolo.keyborad.service.impl;
import com.apple.itunes.storekit.client.APIException;
import com.apple.itunes.storekit.client.AppStoreServerAPIClient;
import com.apple.itunes.storekit.migration.ReceiptUtility;
import com.apple.itunes.storekit.model.Environment;
import com.apple.itunes.storekit.model.JWSTransactionDecodedPayload;
import com.apple.itunes.storekit.model.TransactionInfoResponse;
import com.apple.itunes.storekit.verification.SignedDataVerifier;
import com.apple.itunes.storekit.verification.VerificationException;
import com.yolo.keyborad.model.dto.AppleReceiptValidationResult;
@@ -27,102 +23,67 @@ import java.util.List;
@Service
public class AppleReceiptServiceImpl implements AppleReceiptService {
/**
* App Store 服务器 API 客户端
* <p>
* 用于调用 Apple App Store Server API 获取交易信息
* </p>
*/
private final AppStoreServerAPIClient client;
/**
* 签名数据验证器
* <p>
* 用于验证和解码 Apple 返回的 JWS 签名数据
* 用于验证和解码客户端传来的 JWS 签名交易数据
* </p>
*/
private final SignedDataVerifier signedDataVerifier;
/**
* 收据工具类
* <p>
* 用于解析应用收据内容,提取交易 ID 等信息
* </p>
*/
private final ReceiptUtility receiptUtility;
/**
* 构造函数
* <p>
* 通过构造函数注入所需的依赖组件
* 通过构造函数注入签名数据验证器
* </p>
* @param client App Store 服务器 API 客户端
* @param signedDataVerifier 签名数据验证器
* @param receiptUtility 收据工具类
*/
public AppleReceiptServiceImpl(AppStoreServerAPIClient client,
SignedDataVerifier signedDataVerifier,
ReceiptUtility receiptUtility) {
this.client = client;
public AppleReceiptServiceImpl(SignedDataVerifier signedDataVerifier) {
this.signedDataVerifier = signedDataVerifier;
this.receiptUtility = receiptUtility;
}
/**
* 验证 Apple 应用内购买收据
* 验证 Apple 应用内购买交易
* <p>
* 执行完整的收据验证流程,包括解析收据、获取交易信息、验证签名和业务逻辑校验
* 直接验证从客户端传来的 JWS (JSON Web Signature) 交易数据
* 使用 SignedDataVerifier 验证签名并解码交易信息,然后执行业务逻辑校验
* </p>
* @param appReceipt Base64 编码的应用内购买收
* @param signedTransaction 客户端传来的 JWS 格式的签名交易数
* @return 验证结果对象,包含验证状态和交易信息
*/
@Override
public AppleReceiptValidationResult validateReceipt(String appReceipt) {
// 检查收据是否为空
if (appReceipt == null || appReceipt.isBlank()) {
return invalid("empty_receipt");
public AppleReceiptValidationResult validateReceipt(String signedTransaction) {
// 检查 JWS 是否为空
if (signedTransaction == null || signedTransaction.isBlank()) {
return invalid("empty_transaction");
}
try {
// 1. 从收据里解析出 transactionId不做验证只是解析 ASN.1
String transactionId = receiptUtility.extractTransactionIdFromAppReceipt(appReceipt);
// todo 验证服务器传输的transactionId
// String transactionId = receiptUtility.extractTransactionIdFromTransactionReceipt(appReceipt);
if (transactionId == null) {
return invalid("no_transaction_id_in_receipt");
}
// 1. 使用 SignedDataVerifier 直接验证 JWS 并解码交易 payload
// 这会验证签名、证书链、bundle ID、环境等
JWSTransactionDecodedPayload payload =
signedDataVerifier.verifyAndDecodeTransaction(signedTransaction);
// 2. 调用 App Store Server API 获取单笔交易信息
TransactionInfoResponse infoResponse = client.getTransactionInfo(transactionId);
// 2. 记录交易信息用于调试
log.info("Verified transaction: transactionId={}, productId={}, purchaseDate={}",
payload.getTransactionId(),
payload.getProductId(),
payload.getPurchaseDate());
String signedTransactionInfo = infoResponse.getSignedTransactionInfo();
if (signedTransactionInfo == null) {
return invalid("no_signed_transaction_info");
}
// 3. 使用 SignedDataVerifier 验证 JWS 并解码 payload
JWSTransactionDecodedPayload payload =
signedDataVerifier.verifyAndDecodeTransaction(signedTransactionInfo);
// 4. 执行业务校验:检查交易是否仍然有效
// 3. 执行业务校验:检查交易是否仍然有效
boolean stillActive = isTransactionActive(payload);
// 构建并返回验证结果
// 4. 构建并返回验证结果
return getAppleReceiptValidationResult(stillActive, payload);
} catch (VerificationException e) {
// 验证异常处理
log.warn("Apple receipt verification failed", e);
// 验证异常处理签名无效、证书链问题、bundle ID 不匹配等
log.warn("Apple transaction verification failed: status={}", e.getStatus(), e);
return invalid("verification_exception:" + e.getStatus());
} catch (APIException e) {
// API 异常处理,可以根据错误码进一步处理(如环境切换)
// 4040010 表示订单不存在,可能是环境不匹配导致
long code = e.getApiError().errorCode();
log.warn("App Store API error, code={}", code, e);
return invalid("api_exception:" + code);
} catch (Exception e) {
// 其他未预期异常处理
log.error("Unexpected error when validating Apple receipt", e);
return invalid("unexpected_error");
log.error("Unexpected error when validating Apple transaction", e);
return invalid("unexpected_error:" + e.getMessage());
}
}