这篇主要是回顾一下之前做过的ios app内购,以及在实现过程中遇到的问题
IOS 内购支付有两种模式:
- 内置模式
- 服务器模式
内置模式的流程:
- app从app store 获取产品信息
- 用户选择需要购买的产品
- app发送支付请求到app store
- app store 处理支付请求,并返回transaction信息
- app将购买的内容展示给用户
服务器模式的流程:
- app从服务器获取产品标识列表
- app从app store 获取产品信息
- 用户选择需要购买的产品
- app 发送 支付请求到app store
- app store 处理支付请求,返回transaction信息
- app 将transaction receipt 发送到服务器
- 服务器收到收据后发送到app stroe验证收据的有效性
- app store 返回收据的验证结果
- 根据app store 返回的结果决定用户是否购买成功
上述两种模式的不同之处主要在于:交易的收据验证,内建模式没有专门去验证交易收据,而服务器模式会使用独立的服务器去验证交易收据。
内建模式简单快捷,但容易被破解,服务器模式流程相对复杂,但相对安全。
考虑到安全问题,项目组自然选择服务器模式
开发之初,就知晓苹果支付服务器的不稳定,真实开发后验证了果真没错,甚至不太稳定。苹果支付服务器验证一个支付凭据需要3s-6s。
那么有两个问题:
1.这么长的无响应时间,用户体验太糟糕
2.苹果支付服务器宕机或不稳定,用户如何及时知晓支付是否成功?
解决方案:
1.这么长的时间,用户体验糟糕是肯定的,甚至等很长时间最后是失败的,对此我们采用异步验证的方式,服务器收到客户端请求后,将请求放入MQ中处理
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 许可协议,转载请注明出处!