Java服务端接入苹果内购。实现票据二次校验、自动续期订阅_洋哥登陆的博客-程序员秘密_java 苹果内购

技术标签: 经验分享  java  ios  

简介

记录一下 Java 服务端接入苹果内购。

1. 商品类型

苹果规定在 APP Store上架的 APP 使用苹果自己的支付方式(IAP内购),并且苹果会抽30%的税。
上架商品包括:消耗性,非消耗性,自动续期订阅,非续期订阅。上架商品可在 APP Store后台配置。

2. 获取支付票据

由用户完成付款操作后,苹果返回 票据 给 IOS 客户端,再由客户端返回给服务端进行业务处理。服务端需要携带 票据 信息向苹果进行二次 票据验证,验证成功后可继续进行剩余的业务逻辑。

3. 票据验证

票据是苹果将支付的相关信息,整理成了一个json返回给我们。里面包含比较常用的一些数据段是商品ID、支付时间、苹果的订单ID(transactionId),以及自动订阅商品的优惠政策、过期时间、续订时间等。
苹果有两个票据校验的接口,一个是沙盒环境,一个是正式环境。在测试阶段和上线后需要用不同的接口去校验。正式票据到沙盒环境校验会报 (21007) 的错误码。

注意: 自动订阅模式需要传输 ”共享密钥” 参数,可在APP Store中获取。
官方文接口文档:
https://developer.apple.com/documentation/appstorereceipts/verifyreceipt
https://developer.apple.com/documentation/appstorereceipts/requestbody

4. 服务端验证票据

4.1 服务端逻辑

 public void verifyReceipt(AppleRequestProtocol request) {
    
		// 票据
        String receipt = request.getReceipt();	
		// 服务端自己的订单号,可用做后续业务逻辑
        String orderId = request.getOrderNumber();
		// 注意,有的票据在客户端接收时 加号 可能会被转换为 空格
        String data = receipt.replace(" ", "+");
		// 请求苹果服务器进行票据验证
        String result = AppleVerifyUtil.verifyApple(data, 1, orderId);
        JSONObject receiptData = JSONObject.parseObject(result);
        // 解析票据
        if(result == null){
    
            // 解析票据失败 或 网络问题
			log.error("[ verify receipt error]");
            return ;
        }else {
    
            // 支付环境是否正确
            int status = receiptData.getInteger("status");
            if(21007 == status){
    
                // 验证失败21007 走沙箱环境
                result = AppleVerifyUtil.verifyApple(data, 0);
                if(result == null){
    
                    //  解析票据失败
					log.error("[ verify receipt error]");
                    return ;
                }
                receiptData = JSONObject.parseObject(result);
                status = receiptData.getInteger("status");
            }

			if(0 == status){
    
				JSONObject receiptInfo = receiptData.getJSONObject("receipt");
				JSONArray inAppList = receiptInfo.getJSONArray("in_app");
				if(!CollectionUtils.isEmpty(inAppList)){
    
					JSONObject inApp = inAppList.getJSONObject(inAppList.size() - 1);
					// 票据ID
					String transactionId = inApp.getString("transaction_id");
					// 购买时间
					Long purchaseDateMs = inApp.getLong("purchase_date_ms");
					// 商品ID 与在APP Store 后台配置的一致
					String productId = inApp.getString("product_id");

					// 剩余业务逻辑
				
				}else{
    
					// 获取in_app支付列表失败
					log.error("[receipt error]");
				}
			}
        }
    }

4.2 苹果内购验证工具类

/**
 * 苹果内购验证工具类
 */
@Slf4j
public class AppleVerifyUtil {
    
    /**
     * 苹果内购沙盒环境
     */
    private static final String url_sandbox = "https://sandbox.itunes.apple.com/verifyReceipt";
    /**
     * 苹果内购正式环境
     */
    private static final String url_verify = "https://buy.itunes.apple.com/verifyReceipt";
    /**
     * 秘钥 (自动订阅服务需要秘钥)
     */
    private static final String KEY = "需要到APP Store后台获取";


    /**
     * 苹果服务器内购验证票据
     * @param receipt 验证收据
     * @param type  环境  (0 开发)
     * @return
     */
    public static String verifyApple(String receipt, int type) {
    
        String url = "";
        //环境判断 线上/开发环境用不同的请求链接
        if(type == 0){
    
            url =  url_sandbox;
        }else{
    
            url = url_verify;
        }

        try {
    
            SSLContext sc = SSLContext.getInstance("SSL");
            sc.init(null, new TrustManager[] {
     new TrustAnyTrustManager() }, new java.security.SecureRandom());
            URL console = new URL(url);

            JSONObject jsonObject = new JSONObject();
            jsonObject.put("receipt-data", receipt);
            jsonObject.put("password", KEY);

            OkHttpClient okHttpClient = new OkHttpClient.Builder()
                    .connectTimeout(10, TimeUnit.SECONDS)
                    .readTimeout(10, TimeUnit.SECONDS)
                    .build();
            MediaType mediaType=MediaType.Companion.parse("application/json;charset=utf-8");
            RequestBody stringBody=RequestBody.Companion.create(jsonObject.toString(),mediaType);
            Request request=new Request
                    .Builder()
                    .url(console)
                    .post(stringBody)
                    .build();

            String result = okHttpClient.newCall(request).execute().body().string();
            return result;
        } catch (Exception e) {
    
            log.error("[ios verify error]");
            return null;
        }
    }

    private static class TrustAnyTrustManager implements X509TrustManager {
    

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

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

        @Override
        public X509Certificate[] getAcceptedIssuers() {
    
            return new X509Certificate[] {
    };
        }
    }
}

4.3 票据信息

服务端请求苹果验证接口后,苹果返回解析后的票据信息。重要部分在receipt

"receipt": {
    
        "in_app": [
            {
    
                "product_id": "202201",                             //  商品ID
                "quantity": "1",                                    //  购买商品数量
                "transaction_id": "2000000026612777",               //票据ID
                "original_transaction_id": "2000000026612777",      //原始购买票据ID
                "purchase_date": "2022-04-06 01:54:59 Etc/GMT",     //购买时间
                "purchase_date_ms": "1649210099000",                //购买时间戳
                "purchase_date_pst": "2022-04-05 18:54:59 America/Los_Angeles",         // 购买时间(美国)no

                "original_purchase_date": "2022-04-06 01:55:00 Etc/GMT",                    //原始购买时间
                "original_purchase_date_ms": "1649210100000",                               //原始购买时间戳
                "original_purchase_date_pst": "2022-04-05 18:55:00 America/Los_Angeles",    //原始购买时间(美国)no

                "expires_date": "2022-04-06 01:59:59 Etc/GMT",                  //订阅到期时间
                "expires_date_ms": "1649210399000",                             //订阅到期时间戳
                "expires_date_pst": "2022-04-05 18:59:59 America/Los_Angeles",  //订阅到期时间(美国) no

                "is_in_intro_offer_period": "false",            //是否在享受优惠价格期间
                "is_trial_period": "false",                     //是否享受免费试用
                "web_order_line_item_id": "2000000002007193",   //跨设备购买事件(包括订阅更新事件)的唯一标识符。此值是识别订阅购买的主键
                "in_app_ownership_type": "PURCHASED",
            }
        ],

4.4 错误码

状态码 - 详情
0 校验成功
21000 未使用HTTP POST请求方法向App Store发送请求。
21001 此状态代码不再由App Store发送。
21002 receipt-data属性中的数据格式错误或丢失。
21003 收据无法认证。
21004 您提供的共享密码与您帐户的文件共享密码不匹配。
21005 收据服务器当前不可用。
21006 该收据有效,但订阅已过期。当此状态代码返回到您的服务器时,收据数据也会被解码并作为响应的一部分返回。仅针对自动续订的iOS 6样式的交易收据返回。
21007 该收据来自测试环境,但已发送到生产环境以进行验证。
21008 该收据来自生产环境,但是已发送到测试环境以进行验证。
21009 内部数据访问错误。稍后再试。
21010 找不到或删除了该用户帐户。

5. 续订

针对自动续期订阅类型,App Store会在订阅时间快到期之前,自动扣费帮助用户续订该服务。
server to server的校验方式,也是苹果推荐的校验方式 ,由苹果主动告知我们状态。 服务器需要接收苹果服务器发送过来的回调消息,根据消息类型进行续订,取消订阅,退订等操作。

5.1 配置接收通知地址

需要在App Store connect后台配置订阅状态URL ,用于接收 App Store 服务器回调通知的网址
官方文档: https://help.apple.com/app-store-connect/#/dev0067a330b

5.2 接收通知

苹果服务器通过HTTP POST将JSON对象传递给您的服务器,解析JSON获取responsebody,根据参数 notification_type 通知类型来执行不同的操作。

官方文档
responsebody: https://developer.apple.com/documentation/appstoreservernotifications/responsebodyv1
notification_type: https://developer.apple.com/documentation/appstoreservernotifications/notification_type

public void renewal(JSONObject object) {
    
    // 原始transaction_id
    String originalTransactionId = object.getString("original_transaction_id");
    // 获取订阅通知类型
    String notification_type = object.getString("notification_type");
    log.info("renewal notify: [ original_transaction_id: {} ], [ notification_type: {} ]", originalTransactionId, notification_type);

    // 回调收据信息
    JSONObject unifiedReceipt = object.getJSONObject("unified_receipt");
    JSONArray latestReceiptInfo = unifiedReceipt.getJSONArray("latest_receipt_info");
    if(!CollectionUtils.isEmpty(latestReceiptInfo)) {
    
        JSONObject receipt = latestReceiptInfo.getJSONObject(0);
        String productId = receipt.getString("product_id");
        String transactionId = receipt.getString("transaction_id");

        // 处理自动续订成功
        if("DID_RENEW".equals(notification_type)){
    
           // 业务逻辑
        }

        // 退款
        if("CANCEL".equals(notification_type)){
    
            if(!CollectionUtils.isEmpty(latestReceiptInfo)){
    
                // 业务逻辑
            }
        }
    }
}

参考文档

https://juejin.cn/post/7046969127205863438
苹果简体中文文档: https://developer.apple.com/cn/documentation/

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Peanutfight/article/details/125668096

智能推荐

C++学习笔记4:运算符重载、类型转换 _听说西佳佳难得很的博客-程序员秘密

运算符重载基本概念成员函数重载运算符单目运算符重载双目运算符重载重载++、–运算符重载赋值运算符重载下标运算符重载函数调用运算符“()”友元函数重载运算符成员函数重载运算符和友元函数重载运算符比较类型转换 运算符重载基本概念面向对象程序涉及的重载有函数重载和运算符重载。函数重载是指在相同作用域内,若干个参数特征不同的函数使用相同相同的函数名运算符重载是另一种调用函数...

2020全球顶尖计算机科学家排名发布:香港高校20教授入围,香港科大占6席!_香港科大商学院内地办事处的博客-程序员秘密

近日,Guide2Research 网站发布了 2020 年度全球计算机科学和电子领域顶级科学家排名。该排名旨在为学术社区提供更多可见性,让更多人了解计算机科学领域影响力较大的研究贡献。...

jdk更换ssl证书jssecacerts_向天再借500年V的博客-程序员秘密_jssecacerts证书

jdk更换ssl证书jssecacerts背景项目通过统一身份认证入口进出,项目为http协议,但cas端为https。但突然登录报错,不知原因?报错内容如下:集成cas报错javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderExceptio

动态规划DP算法理解_海霸虾皮王的博客-程序员秘密_dp算法是什么意思

每次决策依赖于当前状态,又随即引起状态的转移,多阶段最优化决策解决问题的过程就称为动态规划。

html文档包含头部正文和尾部,2005-2017年江苏专转本计算机基础历年真题_Lebron Q的博客-程序员秘密

http://www.doczj.com/doc/e3cbea06c950ad02de80d4d8d15abe23482f03e6.htmlB采用并行方式进行数据传输,以提高数据的传输速度10. 下列关于液晶显示器的叙述,错误的是________。A. 它的工作电压低、功耗小B. 它几乎没有辐射C. 它的英文缩写是LCDD. 它与CRT显示器不同,不需要使用显卡11. 下列关于数码相机的叙述,错误...

序列的Z变换_西檬饭的博客-程序员秘密_序列的z变换

通过傅里叶变换,可以实现对离散信号的的频域分析。Z变换时傅里叶变化的推广,对序列和系统做复频域分析

随便推点

文本分类训练集 测试集_半监督文本分类的对抗训练方法_weixin_39880666的博客-程序员秘密

半监督文本分类的对抗训练方法题目:Adversarial Training Methods for Semi-Supervised Text Classification作者:Takeru Miyato, Andrew M. Dai, Ian Goodfellow来源:Published as a conference paper at ICLR 2017 Machine Learning (cs...

Spring AOP : 自动代理创建机制 (APC)_安迪源文的博客-程序员秘密

有时候我们会遇到这样的类似的bean定义的情况: 这些bean需要类似的代理行为,比如都需要被同样的一组拦截器包裹 。如果需要这样定义的bean数量很大,那么我们需要写大量重复度很高的xml或者配置代码来定义这些bean,这显然是个可以优化解决的问题。为此,Spring AOP提供了自动代理创建机制。一个APC其实是一个SmartInstantiationAwareBeanPostProcesso...

linux c 编程手册,Linux C/C++编程手册查阅方法_big maomimkq的博客-程序员秘密

Linux Programmer's Manual & User Commandshttps://www.kernel.org/doc/man-pages/ 搜索框输入epoll调用搜索引擎查找在线搜索查看man手册(比如查epoll),可以直接收藏,方便下次点击搜索:http://global.bing.com/search?q=site:man7.org epollThe Linux ...

Java面向字符的 输入流_seniorShen的博客-程序员秘密_面向字符的输入流

Java中 面向字符的输入流:一、概念字符流是针对字符数据的特点进行过优化的,因而提供一些面向字符的有用特性,字符流的源或目标通常是文本文件。 Reader和Writer是java.io包中所有字符流的父类。由于它们都是抽象类,所以应使用它们的子类来创建实体对象,利用对象来处理相关的读写操作。Reader和Writer的子类又可以分为两大类:一类用来从数据源读入数据或往目的地写出数据(称为节点...

JPA(@Transient @JsonProperty)注解实现对象非持久化,同时可以被序列化的实践_謸氕独尊的博客-程序员秘密_java非持久化注解

JPA(@Transient @JsonProperty)注解实现对象非持久化,同时可以被序列化的实践目的:对象的属性有些被设计为数据库可持久化的字段属性,但是前后端数据传输过程有些字段是不需要被持久化的,所以仅仅被序列化即可。这时只需要组合使用@Transient @JsonProperty场景:代码如下:BusinessDomain DataDomain 这两个对象是不需要持久...