对接银行支付

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

2021-06-11  本文已影响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。

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

上一篇下一篇

猜你喜欢

热点阅读