美文网首页对接银行支付
对接农业银行支付(微信和支付宝)的总结(一)

对接农业银行支付(微信和支付宝)的总结(一)

作者: 天草二十六_简村人 | 来源:发表于2021-06-11 10:25 被阅读0次

    一、背景

    公司的支付平台已对接了微信官方和杭州银行两个支付渠道,介于费率的问题,偏向于使用后者。前段时间,杭州银行该支付通道因为不知名的原因被封,导致只能使用微信官方。
    所以我们迫切需要再介入一家银行,防备下次什么时候被封禁。

    本文将要描述和农行对接的详细步骤以及踩过的那些坑,因为踩过坑,希望后来者不要再浪费那么的时间,后期我也会尽量将对接的sdk上传到github开源。

    二、银行支付

    站在用户的角度,扫码或者JSAPI支付,都是偏向于微信和支付宝,极少数去下载各个银行APP,然后使用银行的APP付款。正因为微信和支付宝的用户群体众多,而且它可以绑定各个银行的银行卡或信用卡进行支付,使得银行方也不得不与之合作。
    所谓合作,就是我们的请求方本来是微信或支付宝,现在是银行方,类似一个代理者的角色。
    用户的支付体验不变,但是钱是付款到了银行方,由银行方和微信或支付宝对接。
    说了这么多,下面简单画一个图,便于小白理解:


    业务方对接多个支付.png

    用户是使用微信付款的,钱在农业银行的卡里,业务方是和农业银行进行对账,不用去管微信和支付宝。

    三、主角登场--农业银行

    本文重在梳理从加载证书--》拼接请求报文(明文+签名)--》发送http--》解析响应报文(可能是密文+验签)的流程来表述。在第三篇文章,我将对他们的对接进行一一吐槽。


    image.png

    3.1 加载证书

    在应用的容器初始化后,遍历配置中的账户列表,入参为商户对象。开始初始化根证书文件abc.truststore,取得TrustManager对象,并赋值给SSLContext。
    有了SSLContext,接下里就是将它赋值给HttpClient的连接池管理器PoolingHttpClientConnectionManager, 并最终将账户列表缓存在Map里,减少读取文件的效率,每次发起交易都去读取证书文件锁引起的耗时是非必要的。

    3.2 发送报文

    这一步就是一个简单的https请求了,Post方式,报文内容格式是JSON。示例报文在后面的文章里有。头信息和请求参数都不需要传,传入的StringEntity的“Content-Type: text/xml”。
    一个HttpUtil.java类就搞定了。
    拼接好了明文后,必须对它进行签名,一并发送给农行。
    签名就必须要用到商户的私钥证书pfx的PrivateKey了。计算签名主要是jdk自带的类java.security.Signature.java

    3.3 处理响应

    收到响应报文,必须验证签名。
    先把签名字段进行base64解码,和使用支付平台证书对响应明文所计算出来的签名,两者比较是否相等。
    如果签名一致,就可以使用json对响应报文进行取值了。

    四、技术点罗列

    公司内部的接口对接,比较简单,除规定http/https的url地址, 还需要说明使用的方法get/post/put/delete,还有一个比较重要的字段就是Content-Type了,指定处理请求的提交内容类型(Content-Type),例如application/json, text/html。
    农行使用的正是xml格式。但是农行的报文内容,又是json格式,除了支付通知回调报文外。

    4.1、签名

    public static String sign(final String inputMessage, final String encoding, final AbcMerchantInfo merchantInfo) {
            String signedMessage = "";
            try {
                Signature tSignature = Signature.getInstance(AbcBankConfig.SIGNATURE_ALGORITHM_VALUE);
                //商户的私钥文件.pfx中读取的PrivateKey           
               tSignature.initSign(merchantInfo.getPrivateKey());
    
                tSignature.update(inputMessage.getBytes(encoding));
    
                final byte[] tSigned = tSignature.sign();
                final Base64 tBase64 = new Base64();
                final String tSignedBase64 = tBase64.encode(tSigned);
    
                signedMessage = "{\"Message\":" + inputMessage + ","
                        + "\"Signature-Algorithm\":" + "\"" + AbcBankConfig.SIGNATURE_ALGORITHM_VALUE + "\"" + ","
                        + "\"Signature\":" + "\"" + tSignedBase64 + "\"}";
    
            } catch (Exception e) {
                log.error("农业银行生成签名出现异常, mchId = {}", merchantInfo.getMerId(), e);
                throw new IllegalArgumentException("农业银行生成签名出现异常", e);
            }
            return signedMessage;
        }
    
    /**
    * 填写pfx的文件路径和读取密码,将私钥信息赋值给AbcMerchantInfo对象
    **/
    public static void bindMerchantCertificateByFile(AbcMerchantInfo merchantInfo, String merPfxFile, String merPfxPassword) {
            try (FileInputStream tIn = new FileInputStream(merPfxFile)) {
                KeyStore tKeyStore = KeyStore.getInstance("PKCS12", new Provider().getName());
                tKeyStore.load(tIn, merPfxPassword.toCharArray());
    
                // 读取证书内容
                String tAliases = "";
                final Enumeration e2 = tKeyStore.aliases();
                if (e2.hasMoreElements()) {
                    tAliases = (String) e2.nextElement();
                }
                Certificate tCert = tKeyStore.getCertificate(tAliases);
                final Base64 tBase64 = new Base64();
                String merCertificate = tBase64.encode(tCert.getEncoded());
                merchantInfo.setMerCertificate(merCertificate);
    
                // 校验证书
                final X509Certificate tX509Cert = (X509Certificate) tCert;
                tX509Cert.checkValidity();
    
                // 读取证书私钥
                PrivateKey privateKey = (PrivateKey) tKeyStore.getKey(tAliases, merPfxPassword.toCharArray());
                merchantInfo.setPrivateKey(privateKey);
    
            } catch (Exception e) {
                log.error("读取农行的商户证书文件出现异常, merPfxFile={}", merPfxFile, e);
                throw new IllegalStateException("读取农行的商户证书文件出现异常", e);
            }
        }
    
    

    4.2、验签

    private static boolean verify(final String tTrxResponse, final String tAlgorithm, final String tSignBase64,
                                      final String encoding, final AbcMerchantInfo merchantInfo) {
            final Base64 tBase64 = new Base64();
            final byte[] tSign = tBase64.decode(tSignBase64);
            try {
                final Signature tSignature = Signature.getInstance(tAlgorithm);
    // TrustPay.cer文件,它是一个java.security.cert.Certificate对象。
                tSignature.initVerify(merchantInfo.getTrustPayCertFile());
    
                tSignature.update(tTrxResponse.getBytes(encoding));
    
                return tSignature.verify(tSign);
            } catch (Exception e) {
                log.error("农业银行校验签名出现异常,mchId = {}", merchantInfo.getMerId(), e);
                throw new IllegalArgumentException("农业银行校验签名出现异常", e);
            }
        }
    
    // 在应用初始化的时候,读取TrustPay.cer文件,然后存放在应用的内存里
                merchantInfo.setTrustPayCertFile(getCertificate(certPath + param.getTrustPayCertFileName()));
    
    public static Certificate getCertificate(final String certFile) {
            Certificate tCertificate = null;
    
            try (FileInputStream tIn = new FileInputStream(certFile)) {
                final byte[] tCertBytes = new byte[4096];
    
                int tCertBytesLen = tIn.read(tCertBytes);
    
                final byte[] tFinalCertBytes = new byte[tCertBytesLen];
                for (int i = 0; i < tCertBytesLen; ++i) {
                    tFinalCertBytes[i] = tCertBytes[i];
                }
                Security.addProvider(new Provider());
    
                final CertificateFactory tCertificateFactory = CertificateFactory.getInstance("X.509");
                final ByteArrayInputStream bais = new ByteArrayInputStream(tFinalCertBytes);
                if (bais.available() > 0) {
                    tCertificate = tCertificateFactory.generateCertificate(bais);
                }
            } catch (Exception e) {
                log.error("加载农行的cert文件出现异常,certFile={}", certFile, e);
                throw new IllegalArgumentException("加载农行的cert文件出现异常", e);
            }
    
            return tCertificate;
        }
    

    4.3、https请求

    官方示例采用的是httpclient3.x,我这里升级到了4.x,因为spring cloud feign基本要使用httpclient的话,也将是4.x了。

    其实有了上面的SSLContext, 想要发起https请求的代码写起来就容易了。网上搜索一大把示例,就不赘述了。

    代码结构.png

    前文也说了,农行一会是xml,一会是json,让你晕头转向不说,关键是在计算签名和验证签名的时候,还基础不对。而它给的示例,就是采用字符串的拼接,并没有去引用json库或者xml库。

    最后我保留了它提供的4个类:Base64.java;Base64Code.java; JSON.java; XMLDocument.java。

    根据上面的步骤,我写了四个类:

    • 类AbcBankSignUtil.java(签名和验签);
    • 类AbcBankReadMerFileUtils.java(读取证书文件,pfx/cer/truststore)
    • 类AbcBankHttpService.java(发送https请求)
    • 类AbcBankCertCache.java(缓存,减少重复的读取证书文件)

    相关文章

      网友评论

        本文标题:对接农业银行支付(微信和支付宝)的总结(一)

        本文链接:https://www.haomeiwen.com/subject/rqmmeltx.html