苹果内购 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

posted @ 2023-02-21 13:46  fy_qxl  阅读(1213)  评论(0编辑  收藏  举报