接入Apple Pay流程

接入Apple Pay流程 最近在做IOS内购的后端事项,所以总结下整个流程,都是参考网上大佬的。 首先我们要搞清楚两个概念:苹果支付(Apple Pay)...

接入Apple Pay流程

最近在做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开发中的那些坑和掉单问题 - 铁蕾的个人博客]

验单返回数据格式

老版本IOS返回

{

"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

}

复制代码

新版IOS返回(7.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