Apple内购Java验签逻辑

一 Apple内购流程

  • 先在Apple开发者后台创建产品,同时与自己系统的产品进行关联
  • 用户在APP端选择购买的产品,在自己系统中进行下单并获取单号
  • 根据Apple产品ID在Apple服务中创建订单并支付,获取到receipt
  • 拿着自己系统的单号和支付后的receipt调用自己服务端验签接口
  • 自己服务端调用Apple服务的验证接口(需要先调用沙盒验证,再调用生产验证;自己测试及APP审核时会走沙盒)
  • 根据验证结果处理自身业务逻辑

在这个流程中客户端也需要考虑一些异常情况,由于我只是一个Javaer,客户端这里的逻辑不进行赘述(专业的事情要交给专业的人去处理)

了解了整个内购的流程之后我们也就能够知道作为后端研发我们要进行哪些工作了。

  • 提供验证接口(如参为订单号和支付凭证)
  • 调用Apple服务的验证接口
  • 根据验证结果处理支付业务逻辑

二 代码展示

Talk is cheap, show me the code!

接口定义很简单,这里就不粘贴了,验证的逻辑需要先验证沙盒,然后根据返回的状态码判断是否需要再调用生产验证接口。因为客户端开发及APP提审时使用的是沙盒支付。伪代码如下:

java 复制代码
...省略
AppleCertificateResultDTO certificateResultDTO = AppleUtil.checkCertificate(receipt, false);

if (Objects.equals(certificateResultDTO.getStatus(), 21007)) {
    certificateResultDTO = AppleUtil.checkCertificate(receipt, true);
}
...省略

1 验签工具类

java 复制代码
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.rise.adventure.trade.domain.dto.pay.AppleCertificateResultDTO;
import com.rise.common.enums.RiseCommonResultCode;
import com.rise.common.exception.RiseException;
import lombok.extern.slf4j.Slf4j;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

@Slf4j
public class AppleUtil {
    //购买凭证验证地址
    private static final String CERTIFICATE_URL = "https://buy.itunes.apple.com/verifyReceipt";

    //测试的购买凭证验证地址
    private static final String TEST_CERTIFICATE_URL = "https://sandbox.itunes.apple.com/verifyReceipt";

    /**
     * 重写X509TrustManager
     */
    private static TrustManager trustManager = new X509TrustManager() {
        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return null;
        }

        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        }

        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        }
    };

    public static AppleCertificateResultDTO checkCertificate(String certificate, boolean qaEnv) {
        String certificateUrl = CERTIFICATE_URL;
        if (qaEnv) {
            certificateUrl = TEST_CERTIFICATE_URL;
        }
        try {
            SSLContext ssl = SSLContext.getInstance("SSL");
            ssl.init(null, new TrustManager[]{trustManager}, null);

            //打开连接
            HttpsURLConnection conn = (HttpsURLConnection) new URL(certificateUrl).openConnection();
            //设置套接工厂
            conn.setSSLSocketFactory(ssl.getSocketFactory());
            //加入数据
            conn.setRequestMethod("POST");
            conn.setDoOutput(true);
            conn.setRequestProperty("Content-type", "application/json");


            JSONObject obj = new JSONObject();
            obj.put("receipt-data", certificate);

            BufferedOutputStream buffOutStr = new BufferedOutputStream(conn.getOutputStream());
            buffOutStr.write(obj.toString().getBytes());
            buffOutStr.flush();
            buffOutStr.close();
            //获取输入流
            BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));

            String line;
            StringBuffer resBuilder = new StringBuffer();
            while ((line = reader.readLine()) != null) {
                resBuilder.append(line);
            }
            log.info("苹果支付验证结果为: 【{}】", resBuilder);
            return JSON.parseObject(resBuilder.toString(), AppleCertificateResultDTO.class);
        } catch (Exception e) {
            log.error("苹果支付验证结果失败: ", e);
            throw new RiseException(RiseCommonResultCode.UNKNOWN, "苹果支付结果验证失败");
        }
    }
}

2 receipt解析结果对象

java 复制代码
@Setter
@Getter
public class AppleCertificateResultDTO implements Serializable {
    private Integer status;

    private String environment;

    private AppleReceiptDTO receipt;
}

@Setter
@Getter
public class AppleReceiptDTO implements Serializable {
    @JSONField(name = "adam_id")
    private String adamId;

    @JSONField(name = "app_item_id")
    private String appItemId;

    @JSONField(name = "application_version")
    private String applicationVersion;

    @JSONField(name = "bundle_id")
    private String bundleId;

    @JSONField(name = "download_id")
    private String downloadId;

    @JSONField(name = "original_application_version")
    private String originalApplicationVersion;

    @JSONField(name = "original_purchase_date")
    private String originalPurchaseDate;

    @JSONField(name = "original_purchase_date_ms")
    private String originalPurchaseDateMs;

    @JSONField(name = "original_purchase_date_pst")
    private String originalPurchaseDatePst;

    @JSONField(name = "receipt_creation_date")
    private String receiptCreationDate;

    @JSONField(name = "receipt_creation_date_ms")
    private String receiptCreationDateMs;

    @JSONField(name = "receipt_creation_date_pst")
    private String receiptCreationDatePst;

    @JSONField(name = "receipt_type")
    private String receiptType;

    @JSONField(name = "request_date")
    private String requestDate;

    @JSONField(name = "request_date_ms")
    private String requestDateMs;

    @JSONField(name = "request_date_pst")
    private String requestDatePst;

    @JSONField(name = "version_external_identifier")
    private String versionExternalIdentifier;

    @JSONField(name = "in_app")
    private List<AppDTO> inApp;

    @Setter
    @Getter
    public static class AppDTO implements Serializable {
        @JSONField(name = "in_app_ownership_type")
        private String inAppOwnershipType;

        @JSONField(name = "is_trial_period")
        private Boolean isTrialPeriod;

        @JSONField(name = "original_purchase_date")
        private String originalPurchaseDate;

        @JSONField(name = "original_purchase_date_ms")
        private String originalPurchaseDateMs;

        @JSONField(name = "original_purchase_date_pst")
        private String originalPurchaseDatePst;

        @JSONField(name = "original_transaction_id")
        private String originalTransactionId;

        @JSONField(name = "product_id")
        private String productId;

        @JSONField(name = "purchase_date")
        private String purchaseDate;

        @JSONField(name = "purchase_date_ms")
        private String purchaseDateMs;

        @JSONField(name = "purchase_date_pst")
        private String purchaseDatePst;

        @JSONField(name = "quantity")
        private Integer quantity;

        @JSONField(name = "transaction_id")
        private String transactionId;
    }
}

三 解析结果status值说明

状态码 描述
0 验证成功
21002 收据无法被验证
21003 收据有效,但不属于开发者
21005 收据已使用
21007 沙盒收据无法用于生产环境
21008 生产环境收据无法用于沙盒环境

Apple内购的流程在后端这块的逻辑应该还是比较简单的,只需要交验下客户端传递的支付凭证即可,然后处理下支付成功的逻辑即可。

如果本文对您有些许的帮助,请大方的关注作者吧。

欢迎大家关注我的公众号为我加油打气:【Bug搬运小能手】。文章会优先在公众号发布。

相关推荐
麻瓜生活睁不开眼3 小时前
Android16修改全局桌面视图边框四直角显示为弧边圆角
android·java·深度学习
dear_bi_MyOnly3 小时前
【MyBatis 操作数据库】
java·数据库·学习·mybatis·学习方法
2601_953720823 小时前
【计算机毕业设计】基于Spring Boot的心理健康咨询与评测系统的设计与实现
java·spring boot·后端
丈剑走天涯4 小时前
JDK 17 正式特性
java·开发语言
东方小月4 小时前
从零开发一个 Coding Agent(四):使用状态机校验大模型事件流
前端·人工智能·后端
圣光SG4 小时前
Java操作题练习(七)
java·开发语言·算法
都叫我大帅哥5 小时前
“让AI修就行”?我劝你收回这句话——来自一位Java博主的硬核反常识
后端
whyfail5 小时前
前端学 Spring Boot(8):接口为什么越用越慢?
前端·spring boot·后端
Muscleheng6 小时前
Spring Boot 3.x 集成 DeepSeek 实现 Function Calling(工具调用)
人工智能·spring boot·后端·ai·spring ai·deepseek