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搬运小能手】。文章会优先在公众号发布。

相关推荐
DEARM LINER13 分钟前
mysql 巧妙的索引
数据库·spring boot·后端·mysql
liuyang-neu1 小时前
力扣 简单 70.爬楼梯
java·算法·leetcode
喵手1 小时前
Java 与 Oracle 数据泵实操:数据导入导出的全方位指南
java·开发语言·oracle
开心工作室_kaic3 小时前
ssm010基于ssm的新能源汽车在线租赁管理系统(论文+源码)_kaic
java·前端·spring boot·后端·汽车
代码吐槽菌3 小时前
基于SSM的汽车客运站管理系统【附源码】
java·开发语言·数据库·spring boot·后端·汽车
zdkdchao3 小时前
jdk,openjdk,oraclejdk
java·开发语言
精致先生4 小时前
问题记录01
java·数据库·mybatis
小魏冬琅4 小时前
探索面向对象的高级特性与设计模式(2/5)
java·开发语言
TT哇4 小时前
【Java】数组的定义与使用
java·开发语言·笔记
look_outs5 小时前
JavaSE笔记2】面向对象
java·开发语言