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

相关推荐
丁卯40419 分钟前
Go语言中使用viper绑定结构体和yaml文件信息时,标签的使用
服务器·后端·golang
chengooooooo20 分钟前
苍穹外卖day8 地址上传 用户下单 订单支付
java·服务器·数据库
李长渊哦23 分钟前
常用的 JVM 参数:配置与优化指南
java·jvm
计算机小白一个23 分钟前
蓝桥杯 Java B 组之设计 LRU 缓存
java·算法·蓝桥杯
南宫生3 小时前
力扣每日一题【算法学习day.132】
java·学习·算法·leetcode
计算机毕设定制辅导-无忧学长4 小时前
Maven 基础环境搭建与配置(一)
java·maven
bing_1584 小时前
简单工厂模式 (Simple Factory Pattern) 在Spring Boot 中的应用
spring boot·后端·简单工厂模式
天上掉下来个程小白4 小时前
案例-14.文件上传-简介
数据库·spring boot·后端·mybatis·状态模式
风与沙的较量丶5 小时前
Java中的局部变量和成员变量在内存中的位置
java·开发语言
m0_748251725 小时前
SpringBoot3 升级介绍
java