这是一篇文摘性文章。
验证苹果支付的代码
方法一:使用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中无法直接看到订单信息,可以用以下方法来查询
- 可以通过账单向苹果发送账单验证,有效可以手动补发
- 用自己的服务器的记录账单列表对
- 利用第三方的TalkingData等交易函数,会自动记录账单数据