apple内购
apple内购
还是先找官方文档...
- 打开开发者网址https://developer.apple.com/
- 网页最下面:英文(In-app purchase),中文(App 内购买项目)
- 选择中文(App 内购买项目配置流程)
- 选择中文(生成共享密钥以验证数据)
- 点击链接中文(通过 App Store 验证收据)
苹果内购相关的文档就在这里和附近了
提示
本文这里主要按照单次购买介绍,订阅的类似
为什么要进行服务端验证
这是官方给出的警告⚠️
警告
Don’t call the App Store server verifyReceipt endpoint from your app. You can’t build a trusted connection between a user’s device and the App Store directly, because you don’t control either end of that connection, which makes it susceptible to a machine-in-the-middle attack.
Google翻译: 不要从您的应用程序调用 App Store 服务器 verifyReceipt 端点。 您无法直接在用户设备和 App Store 之间建立可信连接,因为您无法控制该连接的任何一端,这使得它容易受到中间机器攻击。
购买流程
- 苹果支付,然后客户端会收到苹果收据(BASE64编码的字符串)
- 客户端app请求服务端,将收据给到服务端,服务端拿到收据请求苹果服务器验证收据是否为真
- 服务端验证收据真伪,验证当前支付的交易是否成功,成功则处理支付成功的业务逻辑
请求验证说明
官方文档
Send the receipt data to the app store On your server, create a JSON object with the receipt-data, password, and exclude-old-transactions keys detailed in requestBody.
Submit this JSON object as the payload of an HTTP POST request. Use the test environment URL https://sandbox.itunes.apple.com/verifyReceipt when testing your app in the sandbox and while your application is in review. Use the production URL https://buy.itunes.apple.com/verifyReceipt when your app is live in the App Store. For more information on these endpoints, see verifyReceipt.
Google翻译:
将收据数据发送到应用商店 在您的服务器上,使用 requestBody 中详述的 receipt-data、password 和 exclude-old-transactions 键创建一个 JSON 对象。
提交此 JSON 对象作为 HTTP POST 请求的负载。 在沙盒中测试您的应用程序以及审核您的应用程序时,请使用测试环境 URL https://sandbox.itunes.apple.com/verifyReceipt。 当您的应用程序在 App Store 中上线时,请使用生产 URL https://buy.itunes.apple.com/verifyReceipt。 有关这些端点的更多信息,请参阅 verifyReceipt。
注意
Verify your receipt first with the production URL; then verify with the sandbox URL if you receive a 21007 status code. This approach ensures you don’t have to switch between URLs while your application is tested, reviewed by App Review, or live in the App Store.
Google翻译 首先使用生产 URL 验证您的收据; 如果收到 21007 状态代码,则使用沙盒 URL 进行验证。 这种方法可确保您在测试应用程序、通过 App Review 审查或在 App Store 上架时不必在 URL 之间切换。
验证代码
返回码
- 0 正常
- 21000 App Store不能读取你提供的JSON对象
- 21002 receipt-data域的数据有问题
- 21003 receipt无法通过验证
- 21004 提供的shared secret不匹配你账号中的shared secret
- 21005 receipt服务器当前不可用
- 21006 receipt合法,但是订阅已过期。服务器接收到这个状态码时,receipt数据仍然会解码并一起发送
- 21007 receipt是Sandbox receipt,但却发送至生产系统的验证服务
- 21008 receipt是生产receipt,但却发送至Sandbox环境的验证服务
@Override
@OperationLogger
public ReturnResult verifyReceipt(String receipt) {
Map<String, Object> params = new HashMap<>();
params.put("receipt-data", receipt);
Map<String, String> verifyResult = client.postByJsonToMap("https://buy.itunes.apple.com/verifyReceipt", new HashMap<>(), params);
if (verifyResult == null) {
// 苹果服务器没有返回验证结果
return ReturnResultError.builder().code(ConstantCommon.RETURN_CODE_999).msg("订单不存在").data("").build();
} else {
// 苹果验证有返回结果
log.warn("线上,苹果平台返回JSON:" + verifyResult);
String states = verifyResult.get("status");
if (ConstantCommon.RECEIPT_RETURN_STATUS_21007.equals(states)) {
// 是沙盒环境,应沙盒测试,否则执行下面
verifyResult = client.postByJsonToMap("https://sandbox.itunes.apple.com/verifyReceipt", new HashMap<>(), params);
log.warn("沙盒环境,苹果平台返回JSON:" + verifyResult);
states = verifyResult.get("status");
}
// 前端所提供的收据是有效的 验证成功
if (ConstantCommon.RECEIPT_RETURN_STATUS_0.equals(states)) {
String receiptJson = JSON.toJSONString(verifyResult.get("receipt"));
JSONObject returnJson = JSON.parseObject(receiptJson);
String inApp = returnJson.getString("in_app");
String bundleId = returnJson.getString("bundle_id");
//TODO 验证包结构数据
//bundleId是否是app包名
JSONArray jsonArray = JSONObject.parseArray(inApp);
for (int i = 0; i < jsonArray.size(); i++) {
// 获取订单信息对象
JSONObject targetOrder = jsonArray.getJSONObject(i);
// 获取产品信息
String productId = targetOrder.getString("product_id");
// transaction_id交易号
String transactionId = targetOrder.getString("transaction_id");
if (StringUtils.isNotBlank(productId) && StringUtils.isNotBlank(transactionId)) {
//TODO 并且服务端订单数据也是正常状态(也可以在校验金额等其他信息),修改订单状态,如果是作废、已校验、已发货做其他操作
}
}
}
}
return ReturnResultSuccess.builder().code(ConstantCommon.RETURN_CODE_200).msg("success")
.data("返回详情").build();
}
退款
注意
The body of the POST contains the data elements described in the responseBodyV2 for version 2 notifications, and responseBodyV1 for version 1. Parse them using the following information:
The version 2 response body, responseBodyV2, contains a signedPayload that’s cryptographically signed by the App Store in JSON Web Signature (JWS) format. The JWS format increases security and enables you to decode and validate the signature on your server. The notification data contains transaction and subscription renewal information that the App Store signs in JWS. The App Store Server API and the StoreKit In-App Purchase API use the same JWS-signed format for transaction and subscription status information. For more information about JWS, see the IETF RFC 7515 specification.
The version 1 response body, responseBodyV1, is a JSON object. It includes the receipt that contains the most recent in-app purchase transaction for the app. For more information, see the unified_receipt object.
Google翻译:
POST 的正文包含版本 2 通知的 responseBodyV2 和版本 1 的 responseBodyV1 中描述的数据元素。使用以下信息解析它们:
版本 2 响应主体 responseBodyV2 包含一个 signedPayload,它由 App Store 以 JSON Web 签名 (JWS) 格式加密签名。 JWS 格式提高了安全性并使您能够解码和验证服务器上的签名。 通知数据包含 App Store 在 JWS 中签名的交易和订阅续订信息。 App Store Server API 和 StoreKit In-App Purchase API 对交易和订阅状态信息使用相同的 JWS 签名格式。 有关 JWS 的更多信息,请参阅 IETF RFC 7515 规范。
版本 1 响应主体 responseBodyV1 是一个 JSON 对象。 它包括收据,其中包含应用程序的最新应用程序内购买交易。 有关详细信息,请参阅 unified_receipt 对象。
响应App Store服务器通知
When you set up the endpoints on your server to receive notifications, configure your server to send a response. Use HTTP status codes to indicate whether the App Store server notification post succeeded. Send HTTP 200 if the post was successful. If the post didn’t succeed, send HTTP 50x or 40x to have the App Store retry the notification. Your server isn’t required to return a data value.
If the App Store server doesn’t receive a 200 response from your server after the initial notification attempt, it retries as follows:
For version 1 notifications, it retries three times, at 6, 24, and 48 hours after the previous attempt.
For version 2 notifications, it retries five times, at 1, 12, 24, 48, and 72 hours after the previous attempt.
Google翻译:
当您在服务器上设置端点以接收通知时,请将服务器配置为发送响应。 使用 HTTP 状态代码指示 App Store 服务器通知发布是否成功。 如果发布成功,则发送 HTTP 200。 如果发布不成功,请发送 HTTP 50x 或 40x 让 App Store 重试通知。 您的服务器不需要返回数据值。
如果 App Store 服务器在初始通知尝试后没有从您的服务器收到 200 响应,它会重试如下:
对于版本 1 通知,它会在上次尝试后的 6、24 和 48 小时重试三次。
对于版本 2 通知,它会在上次尝试后的 1、12、24、48 和 72 小时重试五次。
版本1退款
@Override
public ReturnResult refundV1(String data) {
log.warn("苹果退款数据v1:" + data);
String receipt = JSON.toJSONString(JSON.parseObject(data).get("unified_receipt"));
JSONObject parseObject = JSON.parseObject(receipt);
String receiptInfo = JSON.toJSONString(parseObject.get("latest_receipt_info"));
JSONArray parseArray = JSON.parseArray(receiptInfo);
for (Object object : parseArray) {
JSONObject da = JSON.parseObject(JSON.toJSONString(object));
String transactionId = da.get("transaction_id").toString();//平台订单id
//TODO 请求apple服务器验证退款信息
//TODO 根据订单id修改修改订单状态,以及其他操作
}
return ReturnResultSuccess.builder().code(ConstantCommon.RETURN_CODE_200).msg("success").data("")
.count(parseArray.size()).build();
}
版本2退款
苹果支付通知v2跟v1区别还是很大的。首先是流程上v1在收到通知后需要再次调用苹果的http接口校验票据的合法性,而v2版本是通过jws(JSON Web Signature)验证其合法性,不需要再次调用苹果服务,直接公钥验签就行。再次是数据结构上的不同,v1通知 http body里直接给了明文,v2由于是jws ,所以内容本身是不可读的。
signedPayload 就是jws,通过 . 号拼接的,分为三部分,分别是 header,payload,signature。分别 通过下面代码就可以解码看到可读的文本。
用到的依赖:
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
</dependency>
从这里找合适的版本https://mvnrepository.com/
DecodedJWT decodedJWT = JWT.decode(jwt);
String header = new String(Base64.getDecoder().decode(decodedJWT.getHeader()));
苹果支付通知的验签 是通过jwt里面的证书验签的,x5c就是 header解码后的内容,这里使用的是第一个证书。
@Override
public ReturnResult refundV2(String jwt) {
// 拿到 header 中 x5c 数组中第一个
DecodedJWT decodedJWT = JWT.decode(jwt);
String header = new String(Base64.getDecoder().decode(decodedJWT.getHeader()));
String x5cs = JSON.toJSONString(JSON.parseObject(header).get("x5c"));
String x5c = JSON.toJSONString(JSON.parseArray(x5cs).get(0));
// 获取公钥
try {
PublicKey publicKey = getPublicKeyByX5c(x5c);
// 验证 token
Algorithm algorithm = Algorithm.ECDSA256((ECPublicKey) publicKey, null);
algorithm.verify(decodedJWT);
} catch (Exception e) {
e.printStackTrace();
//TODO 修改返回码状态
HttpServletResponse response = ((ServletWebRequest) RequestContextHolder.getRequestAttributes()).getResponse();
response.setStatus(403);
return ReturnResultError.builder().code(ConstantCommon.RETURN_CODE_999).msg("验证失败").data("").build();
}
String payload = new String(Base64.getDecoder().decode(decodedJWT.getPayload()));
String data = JSON.toJSONString(JSON.parseObject(header).get("data"));
//TODO 获取数据,和本地数据对比验证,修改订单状态
return ReturnResultSuccess.builder().code(ConstantCommon.RETURN_CODE_200).msg("验证成功").data("")
.count(ConstantCommon.RETURN_COUNT_1).build();
}
public static PublicKey getPublicKeyByX5c(String x5c) throws CertificateException {
byte[] x5c0Bytes = java.util.Base64.getDecoder().decode(x5c);
CertificateFactory fact = CertificateFactory.getInstance("X.509");
X509Certificate cer = (X509Certificate) fact.generateCertificate(new ByteArrayInputStream(x5c0Bytes));
return cer.getPublicKey();
}
苹果内购就到这里,会时时补充最新数据
内购补充(补充于20230823)
verifyReceipt端点已弃用,验收订单需要调用其他的接口,下面是官方说明

Important
The verifyReceipt endpoint is deprecated. To validate receipts on your server, follow the steps in Validating receipts on the device on your server. To validate in-app purchases on your server without using receipts, call the App Store Server API to get Apple-signed transaction and subscription information for your customers, or verify the AppTransaction and Transaction signed data that your app obtains. You can also get the same signed transaction and subscription information from the App Store Server Notifications V2.
我们根据先前的步骤进入官网即可以看到说明,直接按照说明点击跳转到call the App Store Server API即可看到最新的官方文档
这里只介绍:查看订单相关的文档:history(下拉到对应位置即可)
具体详情可以参考苹果内购-WWDC
- 本文作者: 景兴春
- 本文链接: https://www.jingxc.top/back/payment/apple-pay.html
- 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!