IOS内购验证 (Java版)

  • 此处给各位贴出apple官方文档
    App 内购买项目配置流程
    apple 收据文档
    apple 收据responseBody字段释义

  • IOS内购逻辑图
    IOS内购验证 (Java版)_第1张图片

  • IOS内购验证相关代码

package xxxxx;

/**
 * @description 苹果验证返回结果状态码枚举
 * @Author xc
 * @Date 2021/3/12 18:23
 **/
public enum IosStatusCodeEnum {

    CODE_SUCCESS("0", "收据是有效, 验证成功"),
    CODE_NULL("-1", "苹果服务器没有返回验证结果"),
    CODE_21000("21000", "没有使用HTTP POST请求方法向App Store发出请求"),
    CODE_21001("21001", "这个状态码不再由App Store发送"),
    CODE_21002("21002", "receipt-data属性中的数据格式错误或服务遇到临时问题"),
    CODE_21003("21003", "这张收据无法证实真伪"),
    CODE_21004("21004", "您提供的共享秘密与您的帐户文件中的共享秘密不匹配"),
    CODE_21005("21005", "收据服务器暂时无法提供收据"),
    CODE_21006("21006", "订单是有效的,但订阅服务已经过期。当收到这个信息时,解码后的收据信息也包含在返回内容中"),
    CODE_21007("21007", "这个收据来自测试环境,但是它被发送到生产环境进行验证"),
    CODE_21008("21008", "这个收据来自生产环境,但是它被发送到测试环境进行验证"),
    CODE_21009("21009", "内部数据访问错误"),
    CODE_21010("21010", "用户帐户找不到或已被删除");

    public String code;
    public String desc;
    IosStatusCodeEnum(String code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    public static IosStatusCodeEnum getByCode(String code) {
        for (IosStatusCodeEnum value : values()) {
            if(value.code.equals(code)) return value;
        }
        return null;
    }

}
package xxxxx;

import lombok.Data;

/**
 * @description 所有应用内购买交易的应用内购买收据
 * @Author xc
 * @Date 2021/3/12 18:03
 **/
@Data
public class IosVerifyDTO {

    private String transaction_id; // 交易的唯一标识符
    private String original_purchase_date;
    private String quantity; // 购买的消费品数量
    private String original_transaction_id;
    private String purchase_date_pst; // 应用内购买的时间
    private String original_purchase_date_ms;
    private String purchase_date_ms;
    private String product_id; // IOS内购商品Id
    private String original_purchase_date_pst;
    private String is_trial_period;
    private String purchase_date;

}
package xxxxx;


import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import xxxxx.IosStatusCodeEnum;
import xxxxx.ServiceException;
import xxxxx.EnvironmentUtil;
import xxxxx.JsonUtils;
import xxxxx.IosVerifyDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

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.X509Certificate;
import java.util.Locale;
import java.util.Optional;

/**
 * 
/**
 * @description IOS内购验证业务层
 * @Author xc
 * @Date 2021/3/12 18:45
 **/
@Service
public class IosVerifyService {

    // IOS内购 - 沙箱环境
    private static final String URL_SANDBOX = "https://sandbox.itunes.apple.com/verifyReceipt";
    // IOS内购 - 线上环境
    private static final String URL_VERIFY = "https://buy.itunes.apple.com/verifyReceipt";

    private static class TrustAnyTrustManager implements X509TrustManager {

        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) {
        }

        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType) {
        }

        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return new X509Certificate[] {};
        }
    }

    private static class TrustAnyHostnameVerifier implements HostnameVerifier {
        @Override
        public boolean verify(String hostname, SSLSession session) {
            return true;
        }
    }

    /**
     * 苹果支付验证(接口返回null则未查询到收据信息)
     *
     * @param receipt // 收据
     * @param transactionId // 交易的唯一标识符
     * @return
     */
    public IosVerifyDTO applePayVerify(String receipt, String transactionId) {
        // 当前交易标识符收据信息
        IosVerifyDTO iosVerifyDTO = null;
        String url;
        /**
         * 注 :
         *  EnvironmentUtil 为环境工具类,可根据各位实际情况而定,如没有多环境的项目,
         *     可以看下文章最末尾代码片段,无环境写法
         */
        if(EnvironmentUtil.isProd()){
            url = URL_VERIFY;
        }else {
            url = URL_SANDBOX;
        }
        String verifyResult = buyAppVerify(receipt, url);
        JSONObject resultJob = JSONObject.parseObject(verifyResult);
        String status = resultJob.getString("status"); // 苹果验证返回结果状态码
        /**
         * 注 :
         *  此处写法原因 :项目上线的时候,环境肯定为生产环境,正常也是走苹果线上环境,但是因为上线至 App Store 需要苹果公司人员审核,
         *     他们支付的时候使用的是沙箱支付,如没有此处代码,支付将会失败,上线也会被打回,重新提交审核
         */
        if (IosStatusCodeEnum.CODE_21007.code.equals(status)) {
            verifyResult = buyAppVerify(receipt, URL_SANDBOX);
            resultJob = JSONObject.parseObject(verifyResult);
            status = resultJob.getString("status");
        }
        // 收据有效, 验证成功
        if (!IosStatusCodeEnum.CODE_SUCCESS.code.equals(status)) {
            log.error("【苹果服务器验证失败】返回验证结果。 transactionId: {}, status: {}, msg: {}", transactionId, status, IosStatusCodeEnum.getByCode(status).desc);
            return iosVerifyDTO;
        }
        String resultReceipt = resultJob.getString("receipt");
        JSONObject receiptJson = JSONObject.parseObject(resultReceipt);
        String in_app = receiptJson.getString("in_app"); // 苹果收据列表
        if(StringUtils.isEmpty(in_app)) {
            log.error("【苹果服务器验证失败】未查询到收据信息。 transactionId: {}, status: {}, msg: {}", transactionId, status, IosStatusCodeEnum.getByCode(status).desc);
            return iosVerifyDTO;
        }
        JSONArray in_app_array = JSONArray.parseArray(in_app);
        Optional<Object> optional = in_app_array.stream().filter(r ->
                (JsonUtils.fromJson(r.toString(), IosVerifyDTO.class).getTransaction_id().equals(transactionId))).findFirst();
        if(optional.isPresent()) iosVerifyDTO = JsonUtils.fromJson(optional.get().toString(), IosVerifyDTO.class);
        return iosVerifyDTO;
    }

    /**
     * 苹果服务器验证
     *
     * @param receipt 账单
     * @param url IOS内购核验环境地址
     * @url 要验证的地址
     * @return null 或返回结果
     */
    private String buyAppVerify(String receipt, String url) {
        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","application/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;
            StringBuffer sb = new StringBuffer();
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
            return sb.toString();
        } catch (Exception e) {
            log.error("苹果服务器异常 errMsg: {}", e);
            /**
             * 注 :
             *  此处业务异常类私有不对外,各位可根据个人业务进行处理
             */
            throw new ServiceException("苹果服务器异常");
        }
    }

}
/**
 * 注 :
 *    此处代码为无多环境写法
 *    可以将 IosVerifyService 类中 72-95行代码进行替换
 */
String verifyResult = buyAppVerify(receipt, URL_VERIFY);
JSONObject resultJob = JSONObject.parseObject(verifyResult);
String status = resultJob.getString("status"); // 苹果验证返回结果状态码
if (IosStatusCodeEnum.CODE_21007.code.equals(status)) {
    verifyResult = buyAppVerify(receipt, URL_SANDBOX);
    resultJob = JSONObject.parseObject(verifyResult);
    status = resultJob.getString("status");
}

你可能感兴趣的:(Java,java)