java验证苹果支付收据(转载)

本文详细介绍了两种苹果支付验证方法及代码实现,包括使用HttpsURLConnection和HttpClient的方式,并解析了验证返回的JSON数据,提供了处理漏单的策略。

转自胖哥的整理,地址:http://blog.youkuaiyun.com/cnhome/article/details/79380557

苹果说明文档:https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW1

这是一篇文摘性文章。

验证苹果支付的代码

源自

方法一:使用HttpsURLConnection

响应速度比方法二快。

public static JSONObject verifyReceipt1(String recepit) {  
        return verifyReceipt1("https://buy.itunes.apple.com/verifyReceipt", recepit);  
    }  

    public static JSONObject verifyReceipt1(String url, String receipt) {  
        try {  
            HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection();    
            connection.setRequestMethod("POST");    
            connection.setDoOutput(true);    
            connection.setAllowUserInteraction(false);   
            PrintStream ps = new PrintStream(connection.getOutputStream());    
            ps.print("{\"receipt-data\": \"" + receipt + "\"}");    
            ps.close();    
            BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream()));    
            String str;    
            StringBuffer sb = new StringBuffer();    
            while ((str = br.readLine()) != null) {    
                sb.append(str);      
            }    
            br.close();    
            String resultStr = sb.toString();    
            JSONObject result = JSONObject.parseObject(resultStr);  
            if (result != null && result.getInteger("status") == 21007) {  
                return verifyReceipt1("https://sandbox.itunes.apple.com/verifyReceipt", receipt);  
            }  
            return result;  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
        return null;  
    }  

方法二:使用HttpClient

public static JSONObject verifyReceipt2(String receipt) {  
        return verifyReceipt2("https://buy.itunes.apple.com/verifyReceipt", receipt);  
    }  

    public static JSONObject verifyReceipt2(String url, String receipt) {  
        HttpClient httpClient = new DefaultHttpClient();  
        HttpPost httpPost = new HttpPost(url);  
        try {  
            JSONObject data = new JSONObject();  
            data.put("receipt-data", receipt);  
            StringEntity entity = new StringEntity(data.toJSONString());  
            entity.setContentEncoding("utf-8");  
            entity.setContentType("application/json");  
            httpPost.setEntity(entity);  
            HttpResponse response = httpClient.execute(httpPost);  
            HttpEntity httpEntity = response.getEntity();  
            String resultStr = EntityUtils.toString(httpEntity);  
            JSONObject result = JSONObject.parseObject(resultStr);  
            httpPost.releaseConnection();  
            if (result.getInteger("status") == 21007) {  
                return verifyReceipt2("https://sandbox.itunes.apple.com/verifyReceipt", receipt);  
            }  
            return result;  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
        return null;  
    }  

这里的代码仅仅是从苹果获取了JSON对象,并未进行响应的验证。

支付数据的验证

我们来细细看一下返回的JSON,大概是下边这个样子的:

{
    "status": 0,
    "environment": "Production",
    "receipt": {
        "receipt_type": "Production",
        "adam_id": 2341443613,
        "app_item_id": 2234443613,
        "bundle_id": "com.xxxxx.xxxxx",
        "application_version": "1",
        "download_id": 23456572706673,
        "version_external_ident ifier": 821223402,
        "receipt_creation_date": "2017-01-25 00:52:37 Etc/GMT",
        "receipt_creation_date_ms": "3333897657000",
        "receipt_creation_date_pst": "2017-01-25 17:57:37 America/Los_Angeles",
        "request_date": "2017-01-26 00:57:38 Etc/GMT",
        "request_date_ms": "1445897657000",
        "request_date_pst": "2017-05-29 17:57:38 America/Los_Angeles",
        "original_purchase_date": "2016-01-25 15:37:18 Etc/GMT",
        "original_purchase_ date_ms": "145234568000",
        "original_purchase_date_pst": "2016-01-25 07:37:18 America/Los_Angeles",
        "original_application_version": "12",
        "in_app": [
             {
                 "quantity": "1",
                 "product_id": "xxxxxxxxx",
                 "transaction_id": "110000290198443",
                 "original_transaction_id": "110000290198443",
                 "purchase_date": "2017-01-26 00:23:36 Etc/GMT",
                 "purchase_date_ms": "1496105856000",
                 "purchase_date_pst": "2017-01-26 00:35:30 America/Los_Angeles",
                 "original_purchase_date": "2017-01-26 00:57:36 Etc/GMT",
                 "original_purchase_date_ms": "14347896000",
                 "original_purchase_date_pst": "2017-01-25 17:57:36 America/Los_Angeles",
                 "is_trial_period": "false"
             }
         ]
     }
}

—————————————————————————————————————————————————————————

重点解释一下in_app,此处是导致漏单情况最严重的地方

in_app返回的是JsonArrary,就是如果该用户支付多次,前两次没有校验(没走完Apple Pay的完整流程链),则最近一次校验会把前几次的校验串返回。in_app返回的是空,也说明校验是有效的。

这样就需要处理JsonArrary的数量>1的情况,取最新的交易时间的,而且价格匹配上,进行去充值操作,额外数据的进行数据存储,方便后续用户投诉时,进行查找凭证。

如果用户过来的请求也可以去匹配一下额外数据存储表,如果匹配成功,则可以充值。(谨慎使用,如果客服有权限补单充值的)

transaction_id:最新票据交易号

original_transaction_id:最初的票据交易号

网上也有相同的情况,参数地址:https://www.cnblogs.com/widgetbox/p/8241333.html

看完这个就很好理解上面出现的问题了,也就是说:

验证票据返回的receipt里面的in_app字段,这个字段包含了所有你未完成交易的票据信息。也就是在上面说到的APP完成交易之后,这个票据信息,就会从in_app中消失。

如果APP不完成交易,这个票据信息就会在in_app中一直保留。(这个情况可能仅限于你的商品类型为消耗型)

 

知道了事件的原委,就很好优化解决了,方案有2个

1.对票据返回的in_app数据全部进行处理,没有充值的全部进行充值

2.仅对最新的充值信息进行处理(我们采取的方案)

因为采用二方案:

如果用户仅进行了一次充值,该充值未到账,他不再进行充值了,那么会无法导致。

如果他通过客服的途径已经进行了补充充值,那么他在下一次充值的时候依旧会把之前的产品票据带回,这时候有可能出现重复充值的情况

—————————————————————————————————————————————————————————




解读一下status:

0 正常
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环境的验证服务

不难发现我们可以利用 in_app中的quantity、product_id、transaction_id、purchase_date来对支付内容进行检查,当然了记录下返回的receipt文本串也是个不错的方法。

网上有人用MD5值的方法来防止重复支付,其实transaction_id也是可以做唯一区分的。以下是一部分来自网上的代码,源自

public class IOSAction extends BaseAction{  
    private static final long serialVersionUID = 1L;  

    /** 
     * 客户端向服务器验证 
     *  
     *  
     *   * checkState  A  验证成功有效(返回收据) 
     *             B  账单有效,但己经验证过 
     *             C  服务器数据库中没有此账单(无效账单) 
     *             D  不处理 
     *  
     * @return 
     * @throws IOException  
     */  
    public void IOSVerify() throws IOException  
    {  

        HttpServletRequest request=ServletActionContext.getRequest();  
        HttpServletResponse response=ServletActionContext.getResponse();  
        System.out.println(new  Date().toLocaleString()+"  来自苹果端的验证...");  
        //苹果客户端传上来的收据,是最原据的收据  
        String receipt=request.getParameter("receipt");  
        System.out.println(receipt);  
        //拿到收据的MD5  
        String md5_receipt=MD5.md5Digest(receipt);  
        //默认是无效账单  
        String result=R.BuyState.STATE_C+"#"+md5_receipt;  
        //查询数据库,看是否是己经验证过的账号  
        boolean isExists=DbServiceImpl_PNM.isExistsIOSReceipt(md5_receipt);  
        String verifyResult=null;  
        if(!isExists){  
            String verifyUrl=IOS_Verify.getVerifyURL();  
            verifyResult=IOS_Verify.buyAppVerify(receipt, verifyUrl);  
            //System.out.println(verifyResult);  
            if(verifyResult==null){  
                //苹果服务器没有返回验证结果  
                result=R.BuyState.STATE_D+"#"+md5_receipt;  
            }else{  
                //跟苹果验证有返回结果------------------  
                JSONObject job = JSONObject.fromObject(verifyResult);  
                String states=job.getString("status");  
                if(states.equals("0"))//验证成功  
                {  
                    String r_receipt=job.getString("receipt");  
                    JSONObject returnJson = JSONObject.fromObject(r_receipt);  
                    //产品ID  
                    String product_id=returnJson.getString("product_id");  
                    //数量  
                    String quantity=returnJson.getString("quantity");  
                    //跟苹果的服务器验证成功  
                    result=R.BuyState.STATE_A+"#"+md5_receipt+"_"+product_id+"_"+quantity;  
                    //交易日期  
                    String purchase_date=returnJson.getString("purchase_date");  
                    //保存到数据库  
                    DbServiceImpl_PNM.saveIOSReceipt(md5_receipt, product_id, purchase_date, r_receipt);  
                }else{  
                    //账单无效  
                    result=R.BuyState.STATE_C+"#"+md5_receipt;  
                }  
                //跟苹果验证有返回结果------------------  
            }  
            //传上来的收据有购买信息==end=============  
        }else{  
            //账单有效,但己验证过  
            result=R.BuyState.STATE_B+"#"+md5_receipt;  
        }  
        //返回结果  
        try {  
            System.out.println("验证结果     "+result);  
            System.out.println();  
            response.getWriter().write(result);  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  

    }  
}  

特殊场景的处理

有些特殊场景,还是需要前端配合去做的。下边摘录的内容值得了解。源自

关于漏单

  • 漏单必须要处理,玩家花RMB购买的东西却丢失了,是绝对不能容忍的。所谓的漏单就是玩家已经正常付费,却没有拿到该拿的道具。 
    解决:只要购买成功,便将购买记录(receipt等账单信息)保存下来,然后将账单信息传送给我们游戏服务器,游戏服务器获得账单后,和苹果服务器验证,账单有效的话,回馈给游戏服务器处理,游戏服务器处理后,返回给游戏客户端处理,处理完毕,将本地保存的购买记录删除。
  • 漏单的检测位置 
    解决: 
    2.1 做法1:在任意购买成功之后,顺便检测一次漏单,有漏单数遍处理了。 
    2.2 做法2:是在游戏登陆的时候检测一次漏单,即循环检测漏单数据,挨个发送给服务器验证处理,直到将所有的漏单处理完毕。这是原因是购买服务器未返回结果而客户端崩溃的情况下,玩家再次登陆,会产生漏单。
  • 漏单的版本兼容 
    漏单要做好版本兼容,eg.玩家购买英雄ID为100的英雄,产生了一次漏单,但是一直未再次登陆游戏,由于版权等原因,这个英雄在后期版本中被删除了,如果玩家这是漏单处理,会在服务器获得一个丢弃的英雄,产生数据异常。 
    我的处理是,如果是英雄,检测英雄在本地hero.csv中是否有效,如果有效,检测这个英雄是否已经拥有,如果没有且数据正常,发送给服务器处理漏单,否则丢弃掉这条漏单。 
    还有说苹果服务器漏单过期的说法,不过我没有遇到过,没做处理。
  • 服务器和客户端漏单对应顺序 
    遇到过这种情况,客户端产生了多个漏单,发送给游戏服务器验证,游戏服务器请求苹果服务,苹果服务器返回的receipt的json数据中包含一个所有未处理的订单列表,最后产生的购买数据在最后,客户端的漏单顺序和服务器的验证顺序要保持一致。

确保receipt-data的成功提交与异常处理

建立在IAP Server Model的基础上,并且我们知道手机网络是不稳定的,在付款成功后不能确保把receipt-data一定提交到服务器。如果出现了这样的情况,那就意味着玩家被appstore扣费了,却没收到服务器发放的道具。 
解决这个问题的方法是在客户端提交receipt-data给我们的服务器,让我们的服务器向苹果服务器发送验证请求,验证这个receipt-data账单的有效性. 在没有收到回复之前,客户端必须要把receipt-data保存好,并且定期或在合理的UI界面触发向服务端发起请求,直至收到服务端的回复后删除客户端的receipt账单记录。这里就是我在开头提到的漏单处理了。 
如果是客户端没成功提交receipt-data,那怎么办?就是玩家被扣费了,也收到appstore的消费收据了,却依然没收到游戏道具,于是投诉到游戏客服处。 
这种情况在以往的经验中也会出现,常见的玩家和游戏运营商发生的纠纷。游戏客服向玩家索要游戏账号和appstore的收据单号,通过查询itunes-connect看是否确有这笔订单。如果订单存在,则要联系研发方去查询游戏服务器,看订单号与玩家名是否对应,并且是否已经被使用了,做这一点检查的目的是 为了防止恶意玩家利用已经使用过了的订单号进行欺骗(已验证的账单是可以再次请求验证的,曾经为了测试,将账单手动发给服务器处理并成功),谎称自己没收到商品。这就是上面一节IAP Server Model中红字所提到的安全逻辑的目的。当然了,如果查不到这个订单号,就意味着这个订单确实还没使用过,手动给玩家补发商品即可。 
有朋友问怎么通过itunes-connect查看具体订单,itunes-connect中无法直接看到订单信息,可以用以下方法来查询

  1. 可以通过账单向苹果发送账单验证,有效可以手动补发
  2. 用自己的服务器的记录账单列表对
  3. 利用第三方的TalkingData等交易函数,会自动记录账单数据
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值