java验证苹果支付收据

这是一篇文摘性文章。

验证苹果支付的代码

源自

方法一:使用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"
             }
         ]
     }
}

解读一下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等交易函数,会自动记录账单数据
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值