苹果内购 java后端验证订单(转载)
文章转载自: https://www.jianshu.com/p/05699ff6f042
看前须知
往下看之前先说清楚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; }
其他:
关于内购和沙盒测试的相关说明,详见此链接: https://www.5kuai2.com/it/20221202/28777.html