最近在做IOS内购的后端事项,所以总结下整个流程,都是参考网上大佬的。
首先我们要搞清楚两个概念:苹果支付(Apple Pay)和IOS内购(IAP)
苹果支付:是一种支付的方式。和微信支付、支付宝等一样。
内购:是只要在iPhone App上购买的不是实物产品(也就是虚拟产品如qq币、皮肤、英雄......) 都需要走内购流程,苹果这里面抽走30%(真想说一句,太黑了)。
iOS内购充值,是通过客户端接入iOS的IAP模块(In-App Purchase)后,由客户端发起充值,然后再把充值数据(receipt)发给服务端,最后由服务端远程调用AppStore服务器验证。
我们会发现这里和平时开发微信支付、支付宝支付的流程是不太一样的,微信、支付宝会提供后端的接口,由业务的服务端和支付宝的服务端交互,用户支付成功后,支付宝的服务器会异步给我们的后端通知,而在这里却是苹果客户端直接请求了苹果的服务端,成功后,向业务服务端再二次验证进行业务上扣款等操作的处理。
附:
支付宝支付流程
苹果审核App时,是在沙盒环境下测试。所以,当App提交苹果审核时,服务端需换成沙盒环境,否则就无法通过苹果审核。通常游戏开发商都会搞一个审核服来给苹果审核,这样,审核服用沙盒环境,正式服用正式环境。
但对于很多App应用开发商来说,专门搞一个服务器显然增加了不少成本。其实还是有办法处理的,方法如下:
根据验单返回的 status 字段:
当 status = 21007 时,把请求地址换成沙盒测试地址,再次请求验单。
[iOS内购充值 服务器端处理 – 没有开花的树]
[php处理苹果支付接口回调 - xijieyuan2qi的博客 - CSDN博客]
官方:
[苹果的支付回调的接口文档地址]
客户端内购流程:
[干货 | 关于Apple Pay接入和开发,看这一篇就够了]
[iOS开发支付篇-内购(IAP) - 梁飞宇 - 博客园]
[ios 内购详解(2019) - 简书]
[苹果支付流程]
[iOS 内购(In-App Purchase)总结 | 笑忘书店]
[iOS内购掉单问题 | bomo的开发随笔]
[苹果 IAP 开发中的那些坑和掉单问题 - iOS - 掘金]
[苹果IAP开发中的那些坑和掉单问题 - 铁蕾的个人博客]
{
"receipt": {
"original_purchase_date_pst": "2016-12-03 01:11:01 America/Los_Angeles",
"purchase_date_ms": "1480756261254",
"unique_identifier": "96f51b28f628493709966f33a1fe7ba",
"original_transaction_id": "1000000255766",
"bvrs": "82",
"transaction_id": "1000000255766",
"quantity": "1",
"unique_vendor_identifier": "FE358-1362-40FD-870F-DF788AC5",
"item_id": "11822945",
"product_id": "rjkf_itemid_1",
"purchase_date": "2016-12-03 09:11:01 Etc/GMT",
"original_purchase_date": "2016-12-03 09:11:01 Etc/GMT",
"purchase_date_pst": "2016-12-03 01:11:01 America/Los_Angeles",
"bid": "com.xxx.xxx",
"original_purchase_date_ms": "1480756261254"
},
"status": 0
}
复制代码
{
"status": 0,
"environment": "Sandbox",
"receipt": {
"receipt_type": "ProductionSandbox",
"adam_id": 0,
"app_item_id": 0,
"bundle_id": "com.xxx.xxx",
"application_version": "84",
"download_id": 0,
"version_external_identifier": 0,
"receipt_creation_date": "2016-12-05 08:41:57 Etc/GMT",
"receipt_creation_date_ms": "1480927317000",
"receipt_creation_date_pst": "2016-12-05 00:41:57 America/Los_Angeles",
"request_date": "2016-12-05 08:41:59 Etc/GMT",
"request_date_ms": "1480927319441",
"request_date_pst": "2016-12-05 00:41:59 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": [
{
"quantity": "1",
"product_id": "rjkf_itemid_1",
"transaction_id": "10000003970",
"original_transaction_id": "10000003970",
"purchase_date": "2016-12-05 08:41:57 Etc/GMT",
"purchase_date_ms": "1480927317000",
"purchase_date_pst": "2016-12-05 00:41:57 America/Los_Angeles",
"original_purchase_date": "2016-12-05 08:41:57 Etc/GMT",
"original_purchase_date_ms": "1480927317000",
"original_purchase_date_pst": "2016-12-05 00:41:57 America/Los_Angeles",
"is_trial_period": "false"
}
]
}
}
复制代码
一文中说到
这特么也太坑了,返回的数据格式还会不一样?查了一下资料,大概说是:ios7(这个数字可能不对,印象中的,大概就是这么个意思)以前的版本支付和现在的版本支付,去验证时会返回不同的数据结构。WTF,那么作为服务器端,只能先麻烦点,先判断结构中是否包含:in_app,这部分,如果包含就用下面的结构解析,反之则用第一种结构来处理。如果status=0,我们就可以根据客户端提交上来的订单号,将订单状态进行变更了,并将验证结果返回给客户端。
// 苹果服务验证
package com.miracle9.animal.util;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Locale;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
/**
* 苹果IAP内购验证工具类
* @ClassName: IosVerify
* @Description:Apple Pay
*/
public class IosVerifyUtil {
private static class TrustAnyTrustManager implements X509TrustManager {
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[] {};
}
}
private static class TrustAnyHostnameVerifier implements HostnameVerifier {
public boolean verify(String hostname, SSLSession session) {
return true;
}
}
private static final String url_sandbox = "https://sandbox.itunes.apple.com/verifyReceipt";
private static final String url_verify = "https://buy.itunes.apple.com/verifyReceipt";
/**
* 苹果服务器验证
*
* @param receipt
* 账单
* @url 要验证的地址
* @return null 或返回结果 沙盒 https://sandbox.itunes.apple.com/verifyReceipt
*
*/
public static String buyAppVerify(String receipt,int type) {
//环境判断 线上/开发环境用不同的请求链接
String url = "";
if(type==0){
url = url_sandbox; //沙盒测试
}else{
url = url_verify; //线上测试
}
//String url = EnvUtils.isOnline() ?url_verify : url_sandbox;
try {
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, new TrustManager[] { new TrustAnyTrustManager() }, new java.security.SecureRandom());
URL console = new URL(url);
HttpsURLConnection conn = (HttpsURLConnection) console.openConnection();
conn.setSSLSocketFactory(sc.getSocketFactory());
conn.setHostnameVerifier(new TrustAnyHostnameVerifier());
conn.setRequestMethod("POST");
conn.setRequestProperty("content-type", "text/json");
conn.setRequestProperty("Proxy-Connection", "Keep-Alive");
conn.setDoInput(true);
conn.setDoOutput(true);
BufferedOutputStream hurlBufOus = new BufferedOutputStream(conn.getOutputStream());
//String str = String.format(Locale.CHINA, "{\"receipt-data\":\"" + receipt + "\"}");//拼成固定的格式传给平台
// 上面这个有问题(21002),改成下面这样返回正常。直接将receipt当参数发到苹果验证就行,不用拼格式
String str = String.format(Locale.CHINA, receipt);//拼成固定的格式传给平台
hurlBufOus.write(str.getBytes());
hurlBufOus.flush();
InputStream is = conn.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String line = null;
StringBuffer sb = new StringBuffer();
while ((line = reader.readLine()) != null) {
sb.append(line);
}
return sb.toString();
} catch (Exception ex) {
System.out.println("苹果服务器异常");
ex.printStackTrace();
}
return null;
}
/**
* 用BASE64加密
*
* @param str
* @return
*/
public static String getBASE64(String str) {
byte[] b = str.getBytes();
String s = null;
if (b != null) {
s = new sun.misc.BASE64Encoder().encode(b);
}
return s;
}
}
复制代码
作者:何小H
转载地址:https://juejin.cn/post/6844904001897512967