苹果支付的坑

这篇主要是回顾一下之前做过的ios app内购,以及在实现过程中遇到的问题

IOS 内购支付有两种模式:

  • 内置模式
  • 服务器模式

内置模式的流程:

  1. app从app store 获取产品信息
  2. 用户选择需要购买的产品
  3. app发送支付请求到app store
  4. app store 处理支付请求,并返回transaction信息
  5. app将购买的内容展示给用户

服务器模式的流程:

  1. app从服务器获取产品标识列表
  2. app从app store 获取产品信息
  3. 用户选择需要购买的产品
  4. app 发送 支付请求到app store
  5. app store 处理支付请求,返回transaction信息
  6. app 将transaction receipt 发送到服务器
  7. 服务器收到收据后发送到app stroe验证收据的有效性
  8. app store 返回收据的验证结果
  9. 根据app store 返回的结果决定用户是否购买成功

上述两种模式的不同之处主要在于:交易的收据验证,内建模式没有专门去验证交易收据,而服务器模式会使用独立的服务器去验证交易收据。
内建模式简单快捷,但容易被破解,服务器模式流程相对复杂,但相对安全。
考虑到安全问题,项目组自然选择服务器模式

开发之初,就知晓苹果支付服务器的不稳定,真实开发后验证了果真没错,甚至不太稳定。苹果支付服务器验证一个支付凭据需要3s-6s。
那么有两个问题:
1.这么长的无响应时间,用户体验太糟糕
2.苹果支付服务器宕机或不稳定,用户如何及时知晓支付是否成功?
解决方案:
1.这么长的时间,用户体验糟糕是肯定的,甚至等很长时间最后是失败的,对此我们采用异步验证的方式,服务器收到客户端请求后,将请求放入MQ中处理
2.将支付状态存入数据库,若状态为验证超时,定时任务每隔一定时间去苹果支付服务器验证凭据,确保服务器端能够收到验证结果

准备

  1. 客户端开发人员需注册成为苹果开发者
  2. 客户端在拿到支付凭据之前,需要下载苹果提供的一个支付验证文件并将该文件放置在一个支持Https的服务器上,通过网址进行验证

需要客户端传的值:

//这是个巨长的验证参数
{"receipt-data" : "MIIaYAYJKoZIhvcNAQcC……"}

支付信息验证地址:

#苹果支付沙箱验证地址 :https://sandbox.itunes.apple.com/verifyReceipt
#苹果支付正式验证地址:https://buy.itunes.apple.com/verifyReceipt

服务器在拿到客户端传的参数后,需要拿该参数去苹果支付服务器验证是否购买成功
在实际开发过程中,服务器端通过issandbox字段标识客户端传递的收据是沙盒环境中的收据还是生产环境中的收据。

验证成功返回值样例:

后台可以通过判断返回的JSON传中status的值来简单判断支付成功与否

{
  "status": 0,
  "environment": "Sandbox",
  "receipt": {
    "receipt_type": "ProductionSandbox",
    "adam_id": 0,
    "app_item_id": 0,
    "bundle_id": "com.platomix.MicroBusinessManage",
    "application_version": "2.0.0",
    "download_id": 0,
    "version_external_identifier": 0,
    "receipt_creation_date": "2017-06-06 06:35:27 Etc/GMT",
    "receipt_creation_date_ms": "1496730927000",
    "receipt_creation_date_pst": "2017-06-05 23:35:27 America/Los_Angeles",
    "request_date": "2017-06-06 07:13:26 Etc/GMT",
    "request_date_ms": "1496733206549",
    "request_date_pst": "2017-06-06 00:13:26 America/Los_Angeles",
    "original_purchase_date": "2013-08-01 07:00:00 Etc/GMT",
    "original_purchase_date_ms": "1375340400000",
    "original_purchase_date_pst": "2013-08-01 00:00:00 America/Los_Angeles",
    "original_application_version": "1.0",
    "in_app": []
 }
}

验证失败返回值样例:

 服务器二次验证代码
 * 21000 App Store不能读取你提供的JSON对象
 * 21002 receipt-data域的数据有问题
 * 21003 receipt无法通过验证
 * 21004 提供的shared secret不匹配你账号中的shared secret
 * 21005 receipt服务器当前不可用
 * 21006 receipt合法,但是订阅已过期。服务器接收到这个状态码时,receipt数据仍然会解码并一起发送
 * 21007 receipt是Sandbox receipt,但却发送至生产系统的验证服务
 * 21008 receipt是生产receipt,但却发送至Sandbox环境的验证服务

服务器端部分源码:

//接收客户端传过来的验证环境(沙箱或正式)
String  chooseEnv = formatString(request.get("chooseEnv"));
//发送验证数据到苹果服务器
String result = setIapCertificate(userId,receipt,Boolean.valueOf(chooseEnv)); 
//获取订单号
String outTradeNo = appleOrder(userId,result,appId);

/** 接收iOS端发过来的购买凭证
     * @param userId
     * @param receipt
     * @param chooseEnv
     */
    public String setIapCertificate(String userId, String receipt, boolean chooseEnv){
        if(StringUtils.isEmpty(userId) || StringUtils.isEmpty(receipt)){
            return null;
        }
        String url = null;
        url = chooseEnv == true? certificateUrl:certificateUrlTest;
        final String certificateCode = receipt;
        if(StringUtils.isNotEmpty(certificateCode)){
            return sendHttpsCoon(url, certificateCode);
        }else{
            return null;
        }
    }

/**
 *从苹果服务器返回的数据中解析所要的数据
 **/
private String appleOrder(String userId,String result,String channelNo){
        try {
            //解析苹果服务器返回数据
            Map<String, Object> map = JSONUtils.jsonToPojo(result, HashMap.class);
            //获得订单支付状态
            Integer status = (Integer) map.get("status");
            String productId = null;
            for (Map.Entry<String,Object> entry : map.entrySet()) {
                if (("receipt").equals(entry.getKey())) {
                    LinkedHashMap<String,Object> linkedHashMap = (LinkedHashMap<String, Object>) entry.getValue();
                    for (Map.Entry<String,Object> objectEntry : linkedHashMap.entrySet()) {
                        if (("in_app").equals(objectEntry.getKey())) {
                            ArrayList<LinkedHashMap> arrayList = (ArrayList<LinkedHashMap>) objectEntry.getValue();
                            for (LinkedHashMap hashMap : arrayList){
                                //获得支付订单号
                                productId = hashMap.get("product_id").toString();
                            }
                        }
                    }
                }
            }
            return outTradeNo;
        }catch (RuntimeException e){
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 发送请求
     * @param url
     * @param code
     * @return
     */
    private String sendHttpsCoon(String url, String code){
        if(url.isEmpty()){
            return null;
        }
        try {
            //设置SSLContext
            SSLContext ssl = SSLContext.getInstance("SSL");
            ssl.init(null, new TrustManager[]{myX509TrustManager}, null);
            //打开连接
            HttpsURLConnection conn = (HttpsURLConnection) new URL(url).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", code);

            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 = null;
            StringBuffer sb = new StringBuffer();
            while((line = reader.readLine())!= null){
                sb.append(line);
            }
            return sb.toString();
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 重写X509TrustManager
     */
    private static TrustManager myX509TrustManager = 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 {
        }
    };

可以看到返回的结果中,订单号被包裹了三层Map(丧心病狂)
最后测试验证通过的用户名,和充值金额最好用数据库记录下来,方便公司往后的资金核对。

参考:https://blog.csdn.net/wjsshhx/article/details/73088094

技术讨论 & 疑问建议 & 个人博客

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议,转载请注明出处!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    阳明先生_x阅读 15,967评论 3 119
  • 今天小米对外发布了新款的小米Max 2手机,在配置上小米Max 2搭载高通骁龙625处理器,6.44英寸屏幕,4G...
    科技之心阅读 247评论 0 0
  • 读万卷书 是积累经验 行万里路 是付诸实践 读万代人 是生活实战 因此 有人会说 读万卷书 不如行万里路 行万里路...
    旖旎i阅读 258评论 2 14
  • ​昨天晚上和媳妇看了《战狼2》,今天想写写影评,说说这部电影到底get到了国人的什么点?如此火爆! 第一,开篇引人...
    蹉跎笑我阅读 169评论 1 1
  • 你应该也会觉得图书馆和现实世界是有很大不同的吧。 每每走进一个图书馆,不管在学校的,还是公立的,那些拿着书沉醉的人...
    王子小白阅读 153评论 0 0