Java服务端接入苹果内购。实现票据二次校验、自动续期订阅

这篇具有很好参考价值的文章主要介绍了Java服务端接入苹果内购。实现票据二次校验、自动续期订阅。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

简介

记录一下 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)){
                // 业务逻辑
            }
        }
    }
}

更新

2022-10-16

Java服务端接入苹果内购。实现票据二次校验、自动续期订阅
实践中遇到了 消耗性票据 获取不正确的问题,记录一下。
支付票据(receipt)中 收据列表(in_app)会保留所有 订阅类商品非消耗性商品 信息,且会依次进入列表(最后一位是最新的一次购买记录)。消耗性商品 信息只在未向苹果服务器进行校验时存在,且只存在列表第一项(再次购买 消耗性商品 会替换票据信息)。

2023-01-03

  1. 回调通知最新版本为v2;v1已过时
    官方文档:https://developer.apple.com/documentation/appstoreservernotifications/app_store_server_notifications_v2
  2. 支付票据(receipt)中 收据列表(in_app),v1版本最新测试:订阅类商品原始票据位于最后一位;续订的票据信息从第一位开始往后,也就是说多次续订,最新的票据位于倒数第二位。

参考文档

https://juejin.cn/post/7046969127205863438
苹果简体中文文档: https://developer.apple.com/cn/documentation/文章来源地址https://www.toymoban.com/news/detail-432386.html

到了这里,关于Java服务端接入苹果内购。实现票据二次校验、自动续期订阅的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • iOS_苹果内购详细步骤

    什么是Apple Pay? 简单说是一种“支付工具”。对于国外流行信用卡,Apple Pay很符合美国的国情。但对于中国,微信支付、支付宝支付更加便利符合中国的人的行为习惯。 说到这你可能就理解了,Apple Pay,就是类比支付宝类似的线上线下支付工具。 Apple Pay和 支付宝、微信一样

    2023年04月09日
    浏览(39)
  • Unity 之 接入IOS内购过程解析【文末源码】

    看完此文章你可以了解IOS内购接入全过程,可以学习到Unity从零接入内购功能。另外此博文和文末源码没有涉及到掉单补单部分逻辑。 一台mac系统机器 苹果开发者账号 Unity2019.4.x (不同版本,3步骤略有不同) Xcode (我的版本12.5) PS:若公司已有运营人员在后台操作过了,可以

    2023年04月17日
    浏览(32)
  • Electron中苹果支付 Apple Pay inAppPurchase 内购支付

    正在开发中,开发好了,写一个完整详细的过程,保证无脑集成即可 一般情况下,在你看这篇文章的时候,说明你已经开发的app差不多了。 但是要上架app到Mac App Store,则要在appstoreconnect这个地方创建一个app。 在此处创建一个有两种方法:一是 在mac上下载transfore的一个官方

    2024年01月19日
    浏览(39)
  • 前端vue3——实现二次元人物拼图校验

    大家好,我是yma16,本文分享关于 前端vue3——实现二次元人物拼图校验。 vue3系列相关文章: vue3 + fastapi 实现选择目录所有文件自定义上传到服务器 前端vue2、vue3去掉url路由“ # ”号——nginx配置 csdn新星计划vue3+ts+antd赛道——利用inscode搭建vue3(ts)+antd前端模板 认识vite_vue3 初

    2024年02月04日
    浏览(53)
  • 【Unity】Unity接入内购IAP,提示you are not authorized to set the license key

    接入IAP的时候需要输入谷歌的开发者后台key Unity2020之后有可能会提示:you are not authorized to set the license key 查阅相关内容后(https://forum.unity.com/threads/purchase-you-are-not-authorized-to-set-the-license-key-google-play.954261/) Unity2020后不在Editor上面填写了,改成在Dashboard上输入 打开后输入即

    2024年02月08日
    浏览(49)
  • java服务端如何接入WebSocket?

    日常工作中,我们都是使用http请求,来进行前后交互,那么我们也会有使用websocket来进行前后交互的时候,那么它俩有什么区别呢? WebSocket是双向通信协议,模拟Socket协议,可以双向发送或接受信息 HTTP是单向的 WebSocket是需要浏览器和服务器握手进行建立连接的 而http是浏览

    2024年02月08日
    浏览(41)
  • Java 服务接入「东方通(tongweb)」

    调研东方通(tongweb)中间件,将 Authing 核心 Java 服务接入东方通(tongweb)替换 springboot tomcat 东方通,中国中间件的开拓者和领导者,国内领先的大安全及行业信息化解决方案提供商,以“安全+”、“数据+”和\\\"智慧+\\\"三大产品体系为基础,为客户提供领先的中间件、网络信

    2024年02月11日
    浏览(48)
  • 【前端】vue3 接入antdv表单校验

    1、表单校验是非常常见的需求,能够有效的拦截大部分的错误数据,提升效率。 2、快速的给使用者提示和反馈,用户体验感非常好。 3、成熟的表单校验框架,开发效率高,bug少。 最近使用的是vue3+antdv的架子,仔细探究一下表单校验的问题,总结一下。 框架可能不同,主

    2024年02月11日
    浏览(44)
  • java实现excel的导入导出(带参数校验:非空校验、数据格式校验)

    本次封装引入阿里开源框架EasyExcel,EasyExcel是一个基于Java的简单、省内存的读写Excel的开源项目。在尽可能节约内存的情况下支持读写百M的Excel。 github地址:GitHub - alibaba/easyexcel: 快速、简洁、解决大文件内存溢出的java处理Excel工具 。 64M内存20秒读取75M(46W行25列)的Excel(3.0.2

    2024年02月01日
    浏览(69)
  • java如何优雅的实现参数非空校验,快速实现参数非空校验,使用@valid实现参数非空校验

    在java项目接口中,有些必传参数需要进行非空校验,如果参数过多,代码会繁杂且冗余,如何优雅的对参数进行非空校验,下面是实现流程 用实体类接收参数,使用非空注解编辑参数内容 使用 @Valid 注解对参数进行拦截,整体进行非空校验 如果是SpringBoot项目,引入web开发包

    2024年02月08日
    浏览(49)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包