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

一、背景

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

本文将要描述和农行对接的详细步骤以及踩过的那些坑,因为踩过坑,希望后来者不要再浪费那么的时间,后期我也会尽量将对接的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(缓存,减少重复的读取证书文件)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容