看前须知
往下看之前先说清楚ApplePay和苹果内购不是一回事;
ApplePay:是类似与支付宝、微信等支付等,用于购买实物的一种支付方式;
苹果内购:是用于应用内购买虚拟商品的一种支付方式(需要app开发,手动添加商品),另外内购支付方式苹果官方是要抽取金额的30%;
苹果内购流程分析
1.app端支付完,付款成功
2.app端使用苹果返回的receipt,大概长这个样子{"receipt" :"MIITyQYJKoZIhvcNAQcCoIITujCCE7YCAQExCzAJBgUrDgMCGg……"},请求后端校验接口
3.后端校验接口,使用receipt这个值,向apple服务器开始验证账单
4.通过apple服务器返回的参数,对本地账单进行操作
(以下代码,对应3、4步骤)
1.校验工具类(代码来自网络,基本雷同)
import javax.net.ssl.*;
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;
@Slf4j
public class IosUtil {
private static class TrustAnyTrustManager implements X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
private static class TrustAnyHostnameVerifier implements HostnameVerifier{
@Override
public boolean verify(String s, SSLSession sslSession) {
return true;
}
}
/**
* 苹果服务器验证
* @param receipt 账单
* @param url 请求地址
* @return
*/
public static String appBuyVerify(String receipt , String url){
try {
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null,new TrustManager[]{ new TrustAnyTrustManager()},new java.security.SecureRandom());
URL console = new URL(url);
HttpsURLConnection conn = (HttpsURLConnection)console.openConnection();
conn.setSSLSocketFactory(sslContext.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 + "\"}");//拼成固定的格式传给平台
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 e){
log.error("苹果服务器验证出错:{}",e.getMessage());
}
return null;
}
}
2.校验方法
2.1 验单返回数据格式参考
新版IOS返回(7.0以后)
{
"receipt": {
"receipt_type": "ProductionSandbox",
"adam_id": 0,
"app_item_id": 0,
"bundle_id": "com.xxxx.xxxx",
"application_version": "1",
"download_id": 0,
"version_external_identifier": 0,
"receipt_creation_date": "2021-11-01 09:20:51 Etc/GMT",
"receipt_creation_date_ms": "1635758451000",
"receipt_creation_date_pst": "2021-11-01 02:20:51 America/Los_Angeles",
"request_date": "2021-11-01 09:20:52 Etc/GMT",
"request_date_ms": "1635758452973",
"request_date_pst": "2021-11-01 02:20:52 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": "smt_rsxl_y30",
"transaction_id": "1000000901786189",
"original_transaction_id": "1000000901786189",
"purchase_date": "2021-11-01 09:13:33 Etc/GMT",
"purchase_date_ms": "1635758013000",
"purchase_date_pst": "2021-11-01 02:13:33 America/Los_Angeles",
"original_purchase_date": "2021-11-01 09:13:33 Etc/GMT",
"original_purchase_date_ms": "1635758013000",
"original_purchase_date_pst": "2021-11-01 02:13:33 America/Los_Angeles",
"is_trial_period": "false",
"in_app_ownership_type": "PURCHASED"
}]
},
"environment": "Sandbox",
"status": 0
}
老版本IOS返回
{
"receipt": {
"original_purchase_date_pst": "2021-11-01 02:13:33 America/Los_Angeles",
"purchase_date_ms": "1635758013000",
"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": "2021-11-01 09:13:33 Etc/GMT",
"purchase_date_pst": "2021-11-01 02:13:33 America/Los_Angeles",
"bid": "com.xxx.xxx",
"original_purchase_date_ms": "1480756261254"
},
"status": 0
}
2.2 验单返回数状态参考
* 状态码:
21000 对 App Store 的请求不是使用 HTTP POST 请求方法发出的。
21001 App Store 不再发送此状态代码。
21002 receipt-data属性中的数据格式错误或服务遇到临时问题。再试一次。
21003 收据无法验证。
21004 您提供的共享机密与您帐户中存档的共享机密不匹配。
21005 收据服务器暂时无法提供收据。再试一次。
21006 此收据有效,但订阅已过期。当此状态代码返回到您的服务器时,接收数据也会被解码并作为响应 的一部分返回。仅针对自动续订订阅的 iOS 6 样式交易收据返回。
21007 这个收据是来自测试环境,但是是送到生产环境去验证的。
21008 这个收据是来自生产环境,但是被送到了测试环境进行验证。
21009 内部数据访问错误。稍后再试。
21010 用户帐户无法找到或已被删除。
状态代码21100-21199是各种内部数据访问错误。
2.3 service层代码参考
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import javax.servlet.http.HttpServletRequest;
@Component
@Slf4j
public class IosConfig {
//测试环境
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
* @return
*/
public Response applePayOrderNotify(HttpServletRequest request, String receipt) {
String result = IosUtil.appBuyVerify(receipt, url_verify);
if (ConvertUtils.isEmpty(result)) {
log.info("无订单消息");
throw new BusinessException("无订单消息");
} else {
JSONObject job = JSONObject.parseObject(result);
log.info("第一次请求苹果平台返回值:{}" + job);
String status = job.getString("status");
/**
* 请先使用生产 URL 验证您的收据;如果收到 21007 状态代码,
* 再使用沙盒 URL 进行验证。
* 这种方法可以确保您不必在 app 的测试期间、App Review 审核期间或已在 App Store 上架后切换 URL。
*/
if (status.equals("21007")) {
result = IosUtil.appBuyVerify(receipt, url_sandbox);
log.info("沙盒环境,苹果平台返回JSON:{}" + result);
job = JSONObject.parseObject(result);
insertOrder(request, job);
return new Response().success(null);
}
if (status.equals("0")) {
log.info("数据有效,验证成功");
insertOrder(request, job);
return new Response().success(null);
}
throw new BusinessException("订单无效");
}
}
@Transactional(rollbackFor = Exception.class)
public void insertOrder(HttpServletRequest request, JSONObject job) {
String receipt = job.getString("receipt");
JSONObject receiptJson = JSONObject.parseObject(receipt);
String in_app = receiptJson.getString("in_app");
String product_id;
String transaction_id;
//考虑新老版本返回格式
if (ConvertUtils.isEmpty(in_app)) {
product_id = receiptJson.getString("product_id");
transaction_id = receiptJson.getString("transaction_id"); // 订单号
} else {
JSONObject in_appJson = JSONObject.parseObject(in_app.substring(1, in_app.length() - 1));
product_id = in_appJson.getString("product_id");
transaction_id = in_appJson.getString("transaction_id"); // 订单号
}
//获取商品id 和订单号以后 此处可以处理本地的订单逻辑
log.info("apple 验证订单成功");
}
}
3.新增接口
@ApiOperation("苹果内购支付回调(app调用,不是官方回调)")
@GetMapping("applepay_call_back")
public Response applePayOrderCallBack(@RequestParam(value = "receipt") String receipt ,HttpServletRequest request){
log.info("AliPayController.applePayOrderCallBack receipt:{}",receipt);
Response response = iosConfig.applePayOrderNotify(request , receipt);
return response;
}