All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.redkalex.pay.GooglePayService Maven / Gradle / Ivy

There is a newer version: 2.7.7
Show newest version
/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package org.redkalex.pay;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.X509EncodedKeySpec;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.logging.*;
import org.redkale.annotation.*;
import org.redkale.annotation.AutoLoad;
import org.redkale.annotation.Comment;
import org.redkale.convert.json.JsonConvert;
import org.redkale.inject.ResourceEvent;
import org.redkale.service.Local;
import org.redkale.util.*;
import static org.redkalex.pay.PayRetCodes.*;
import static org.redkalex.pay.Pays.PAYTYPE_GOOGLE;

/**
 * 详情见: https://redkale.org
 *
 * @author zhangjx
 */
@Local
@AutoLoad(false)
public final class GooglePayService extends AbstractPayService {

    protected static final String format = "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS"; // yyyy-MM-dd HH:mm:ss

    // 原始的配置
    protected Properties elementProps = new Properties();

    // 配置对象集合
    protected Map elements = new HashMap<>();

    @Resource
    @Comment("必须存在全局配置项,@ResourceListener才会起作用")
    protected Environment environment;

    @Resource(name = "pay.google.conf", required = false) // 支付配置文件路径
    protected String conf = "config.properties";

    @Resource(name = "APP_HOME")
    protected File home;

    @Resource
    protected JsonConvert convert;

    //    public static void main(String[] args) throws Throwable {
    //        String json = "{\n"
    //            + "  \"installed\": {\n"
    //            + "    \"client_id\": \"223352421296-8fta4mjf2c49mijhj3n7o3kjkplt7clq.apps.googleusercontent.com\",\n"
    //            + "    \"client_secret\": \"8E7zJlSDniucmceGhThKO0PS\",\n"
    //            + "    \"redirect_uris\": [],\n"
    //            + "    \"auth_uri\": \"https://accounts.google.com/o/oauth2/auth\",\n"
    //            + "    \"token_uri\": \"https://accounts.google.com/o/oauth2/token\"\n"
    //            + "  }\n"
    //            + "}";
    //
    //    }
    //
    //    private static final String DATA_STORE_SYSTEM_PROPERTY = "user.home";
    //    private static final String DATA_STORE_FILE = ".store/android_publisher_api";
    //    private static final File DATA_STORE_DIR =
    //            new File(System.getProperty(DATA_STORE_SYSTEM_PROPERTY), DATA_STORE_FILE);
    //
    //    /** Global instance of the JSON factory. */
    //    private static final com.google.api.client.json.JsonFactory JSON_FACTORY =
    // com.google.api.client.json.jackson2.JacksonFactory.getDefaultInstance();
    //
    //    /** Global instance of the HTTP transport. */
    //    private static HttpTransport HTTP_TRANSPORT;
    //
    //    /** Installed application user ID. */
    //    private static final String INST_APP_USER_ID = "user";
    //
    //    /**
    //     * Global instance of the {@link DataStoreFactory}. The best practice is to
    //     * make it a single globally shared instance across your application.
    //     */
    //    private static FileDataStoreFactory dataStoreFactory;
    //
    //    private Credential authorizeWithServiceAccount(String serviceAccountEmail) throws GeneralSecurityException,
    // IOException {
    //        logger.info(String.format("Authorizing using Service Account: %s", serviceAccountEmail));
    //
    //        // Build service account credential.
    //        GoogleCredential credential = new GoogleCredential.Builder()
    //            .setTransport(HTTP_TRANSPORT)
    //            .setJsonFactory(JSON_FACTORY)
    //            .setServiceAccountId(serviceAccountEmail)
    //            .setServiceAccountScopes(
    //                Collections.singleton(AndroidPublisherScopes.ANDROIDPUBLISHER))
    //            .setServiceAccountPrivateKeyFromP12File(new File(SRC_RESOURCES_KEY_P12))
    //            .build();
    //        return credential;
    //    }
    //
    //    /**
    //     * Authorizes the installed application to access user's protected data.
    //     *
    //     * @throws IOException
    //     * @throws GeneralSecurityException
    //     */
    //    private Credential authorizeWithInstalledApplication() throws IOException {
    //        logger.info("Authorizing using installed application");
    //
    //        // load client secrets
    //        GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(JSON_FACTORY,
    //            new InputStreamReader(
    //                AndroidPublisherHelper.class
    //                    .getResourceAsStream(RESOURCES_CLIENT_SECRETS_JSON)));
    //        // Ensure file has been filled out.
    //        checkClientSecretsFile(clientSecrets);
    //
    //        dataStoreFactory = new FileDataStoreFactory(DATA_STORE_DIR);
    //
    //        // set up authorization code flow
    //        GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder(HTTP_TRANSPORT,
    //            JSON_FACTORY, clientSecrets,
    // Collections.singleton(AndroidPublisherScopes.ANDROIDPUBLISHER))
    //            .setDataStoreFactory(dataStoreFactory).build();
    //        // authorize
    //        return new AuthorizationCodeInstalledApp(flow, new LocalServerReceiver()).authorize(INST_APP_USER_ID);
    //    }
    @Override
    public void init(AnyValue conf) {
        super.init(conf);
        if (this.convert == null) {
            this.convert = JsonConvert.root();
        }
        this.reloadConfig(Pays.PAYTYPE_GOOGLE);
    }

    @Override
    @Comment("判断是否支持指定支付类型")
    public boolean supportPayType(final short payType) {
        return payType == PAYTYPE_GOOGLE && !elements.isEmpty();
    }

    @Override
    @Comment("重新加载本地文件配置")
    public void reloadConfig(short payType) {
        Properties properties = new Properties();
        if (this.conf != null && !this.conf.isEmpty()) { // 存在Google支付配置
            try {
                File file = (this.conf.indexOf('/') == 0 || this.conf.indexOf(':') > 0)
                        ? new File(this.conf)
                        : new File(home, "conf/" + this.conf);
                InputStream in = (file.isFile() && file.canRead())
                        ? new FileInputStream(file)
                        : getClass().getResourceAsStream("/META-INF/" + this.conf);
                if (in != null) {
                    properties.load(in);
                    in.close();
                }
            } catch (Exception e) {
                logger.log(Level.SEVERE, "init google conf error", e);
            }
        }
        this.environment.forEach(k -> k.startsWith("pay.google."), (k, v) -> properties.put(k, v));
        this.elements = GoogleElement.create(logger, properties);
        this.elementProps = properties;
    }

    @ResourceChanged //     //
    @Comment("通过配置中心更改配置后的回调")
    void onResourceChanged(ResourceEvent[] events) {
        Properties changeProps = new Properties();
        changeProps.putAll(this.elementProps);
        StringBuilder sb = new StringBuilder();
        for (ResourceEvent event : events) {
            if (event.name().startsWith("pay.google.")) {
                changeProps.put(event.name(), event.newValue().toString());
                sb.append("@Resource change '")
                        .append(event.name())
                        .append("' to '")
                        .append(event.coverNewValue())
                        .append("'\r\n");
            }
        }
        if (sb.length() < 1) {
            return; // 无相关配置变化
        }
        logger.log(Level.INFO, sb.toString());
        this.elements = GoogleElement.create(logger, changeProps);
        this.elementProps = changeProps;
    }

    public void setPayElements(Map elements) {
        this.elements = elements;
    }

    public void putPayElements(Map elements) {
        this.elements.putAll(elements);
    }

    @Override
    public GoogleElement getPayElement(String appid) {
        return this.elements.get(appid);
    }

    public void setPayElement(String appid, GoogleElement element) {
        this.elements.put(appid, element);
    }

    public boolean existsPayElement(String appid) {
        return this.elements != null && this.elements.containsKey(appid);
    }

    @Override
    public CompletableFuture prepayAsync(PayPreRequest request) {
        request.checkVaild();
        final PayPreResponse result = new PayPreResponse();
        final GoogleElement element = elements.get(request.getAppid());
        if (element == null) {
            return result.retcode(RETPAY_CONF_ERROR).toFuture();
        }
        result.setAppid(element.appid);
        //        final Map rmap = new TreeMap<>();
        //        rmap.put("content", request.getPayno());
        //        result.setResult(rmap);
        return result.toFuture();
    }

    @Override
    public CompletableFuture notifyAsync(PayNotifyRequest request) {
        String strbean;
        if (request.getAttach() != null && request.getAttach().containsKey("bean")) {
            strbean = request.attach("bean");
        } else {
            strbean = request.getBody();
        }
        if (strbean != null && !strbean.isEmpty()) {
            final GoogleElement element = elements.get(request.getAppid());
            GoogleNotifyBean bean = JsonConvert.root().convertFrom(GoogleNotifyBean.class, strbean);
            PayNotifyResponse resp = new PayNotifyResponse();
            resp.setResponseText(strbean);
            resp.setPayno(bean.payno());
            resp.setPayType(request.getPayType());
            if (!checkSign(element, bean.purchaseData, bean.signature)) {
                return resp.retcode(RETPAY_FALSIFY_ERROR).notifytext(strbean).toFuture();
            }
            return CompletableFuture.completedFuture(resp);
        }
        return CompletableFuture.failedFuture(new UnsupportedOperationException("Not supported yet."));
    }

    @Override
    public CompletableFuture createAsync(PayCreatRequest request) {
        return CompletableFuture.failedFuture(new UnsupportedOperationException("Not supported yet."));
    }

    @Override
    public CompletableFuture queryAsync(PayRequest request) {
        return CompletableFuture.completedFuture(null);
    }

    @Override
    public CompletableFuture closeAsync(PayCloseRequest request) {
        return CompletableFuture.failedFuture(new UnsupportedOperationException("Not supported yet."));
    }

    @Override
    public CompletableFuture refundAsync(PayRefundRequest request) {
        return CompletableFuture.failedFuture(new UnsupportedOperationException("Not supported yet."));
    }

    @Override
    public CompletableFuture queryRefundAsync(PayRefundQryReq request) {
        return CompletableFuture.failedFuture(new UnsupportedOperationException("Not supported yet."));
    }

    @Override
    public PayPreResponse prepay(final PayPreRequest request) {
        return prepayAsync(request).join();
    }

    @Override
    public PayNotifyResponse notify(PayNotifyRequest request) {
        return notifyAsync(request).join();
    }

    @Override
    public PayCreatResponse create(PayCreatRequest request) {
        return createAsync(request).join();
    }

    @Override
    public PayQueryResponse query(PayRequest request) {
        return queryAsync(request).join();
    }

    @Override
    public PayResponse close(PayCloseRequest request) {
        return closeAsync(request).join();
    }

    @Override
    public PayRefundResponse refund(PayRefundRequest request) {
        return refundAsync(request).join();
    }

    @Override
    public PayRefundResponse queryRefund(PayRefundQryReq request) {
        return queryRefundAsync(request).join();
    }

    @Override
    protected String createSign(AbstractPayService.PayElement element, Map map, String text) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    @Override
    protected boolean checkSign(
            AbstractPayService.PayElement element,
            Map map,
            String text,
            Map respHeaders) {
        return checkSign((GoogleElement) element, (String) map.get("purchaseData"), (String) map.get("signature"));
    }

    protected boolean checkSign(GoogleElement element, String purchaseData, String sign) {
        try {
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            PublicKey pubKey = keyFactory.generatePublic(new X509EncodedKeySpec(element.pubkey));
            Signature signature = Signature.getInstance("SHA1WithRSA");
            signature.initVerify(pubKey);
            signature.update(purchaseData.getBytes(StandardCharsets.UTF_8));
            return signature.verify(Base64.getDecoder().decode(sign));
        } catch (Exception e) {
            return false;
        }
    }

    public static class GoogleNotifyBean implements Serializable {

        public String payno; // payno

        public String purchaseData;

        public String signature;

        public String signature() {
            return signature;
        }

        public String payno() {
            return payno;
        }

        @Override
        public String toString() {
            return JsonConvert.root().convertTo(this);
        }
    }

    public static class GoogleElement extends AbstractPayService.PayElement {

        // pay.google.[x].appid
        public String appid = ""; // APP应用ID
        // pay.google.[x].publickey

        public byte[] pubkey; // publicKey

        // pay.google.[x].certpath
        public String certpath = ""; // 签名算法需要用到的密钥

        public static Map create(Logger logger, Properties properties) {
            String def_appid = properties.getProperty("pay.google.appid", "").trim();
            String def_publickey =
                    properties.getProperty("pay.google.publickey", "").trim();
            String def_certpath =
                    properties.getProperty("pay.google.certpath", "").trim();
            String def_notifyurl =
                    properties.getProperty("pay.google.notifyurl", "").trim();

            final Map map = new HashMap<>();
            properties.keySet().stream()
                    .filter(x -> x.toString().startsWith("pay.google.")
                            && x.toString().endsWith(".appid"))
                    .forEach(appid_key -> {
                        final String prefix = appid_key
                                .toString()
                                .substring(0, appid_key.toString().length() - ".appid".length());

                        String appid = properties
                                .getProperty(prefix + ".appid", def_appid)
                                .trim();
                        String publickey = properties
                                .getProperty(prefix + ".publickey", def_publickey)
                                .trim();
                        String certpath = properties
                                .getProperty(prefix + ".certpath", def_certpath)
                                .trim();
                        String notifyurl = properties
                                .getProperty(prefix + ".notifyurl", def_notifyurl)
                                .trim();

                        if (appid.isEmpty() || certpath.isEmpty()) {
                            logger.log(Level.WARNING, properties + "; has illegal google conf by prefix" + prefix);
                            return;
                        }
                        GoogleElement element = new GoogleElement();
                        element.appid = appid;
                        element.pubkey = Base64.getDecoder().decode(publickey);
                        element.certpath = certpath;
                        element.notifyurl = notifyurl;
                        if (element.initElement(logger, null)) {
                            map.put(appid, element);
                            if (def_appid.equals(appid)) {
                                map.put("", element);
                            }
                        }
                    });
            return map;
        }

        @Override
        public boolean initElement(Logger logger, File home) {
            return true;
        }

        @Override
        public String toString() {
            return JsonConvert.root().convertTo(this);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy