通联支付踩坑实录(nodejs版)

前情提要:最新一个项目需要对接通联支付,对方给过来的接口文档特别繁琐,还有一个实例php代码,但是在Node环境下,有一些方法实现略有不同,通过一天的奋斗终于跑通,特别记录下对应的坑和实现方法。

 

主要流程:先根据文档上需要的字段(head&request块)的数据组装完成后转成xml格式,然后对xml数据进行私钥签名,再将签名结果加入到head节中的sign_code字段中,然后对整个xml数据使用一个24位的秘钥进行des加密,然后再将这个24的秘钥进行一次公钥加密后放入一个固定格式的xml数据中,最后对这个大的xml数据进行一次base64编码后,post到通联服务端。

首先,准备工作。

由于对接方式是RSA非对称加密+DES加密,所以对拿到的私钥证书(.pfx)和公钥证书(.cer)进行提取pem,以便后续流程使用。

pfx证书导出pem私钥:

      首先,先导出 证书key:

openssl pkcs12 -in wts.pfx -nodes -out wts_sign.key

      其次,再通过key导出pem私钥:

openssl rsa -in wts_sign.key -nodes -out wts_pri_rsa.pem

      最后,因为RSA算法使用的是pkcs8模式补足,需要对提取的私钥进一步处理:

openssl pkcs8 -topk8 -inform PEM -in wts_pri_rsa.pem -outform PEM -nocrypt > wts_pri.pem

 

cer证书导出pem公钥:

openssl x509 -in wts.cer -pubkey  -noout > wts_pub.pem

具体生成pem证书等问题详情百度,这里不再赘述,若有需要可以留言。

下面来看正式代码处理逻辑,首先,按文档和实例代码的要求,对请求数据进行排列和组装,这部分按照文档来就行,直到走到实例代码获取签名这一步:

//生成签名
  $privateKey = file_get_contents("../wts.pfx");//(pfx文件)
  $sign_code = genSign($xml,$privateKey);
  $head['sign_code'] = $sign_code;
----------------------genSign函数-------------------
//生成 sha1WithRSA 签名
function genSign($toSign,$privateKey){
    openssl_pkcs12_read($privateKey, $certs, "证书密码");
    if(!$certs) exit(0);
    $signature = '';
    openssl_sign($toSign, $signature, $certs['pkey']);
    $signature = base64_encode($signature);
    return $signature;
}

这里在php中,直接使用openssl_sign就可以实现,但是在nodejs中,却不太好处理,特别是使用哪种算法和iv/key的长度,直接上代码,大家少走弯路:

// 安全签名  
  let signCode = this.sha1Sign(makeXml, this.rsaPrivate)
  console.log('allinpay sign result1 ', makeXml, signCode);
-------------------sha1Sign函数---------------------
    sha1Sign(str, prikey) {
        const crypto = require('crypto');
        console.log('>>>>>>>>>>使用 crypto sha1 签名>>>>>>>>>>');
        const sign = crypto.createSign('SHA1');
        sign.update(str);
        sign.end();
        const signature = sign.sign(prikey);
        let result = signature.toString('base64')
        console.log(result);
        return result;
    }

makeXml 为生成好的xml格式数据,xml字符串必须要去除换行及空格,千万注意!!!

this.rsaPrivate为读取到的上面的wts_pri的文件内容。

如果没有太大问题,这步就算走通了,咱们继续往下走,到了加密正文的部分了

//以DESede算法生成一个168位的对称密钥,使用这个密钥对报文进行加密,加密算法为DESede/ECB/PKCS5Padding;
$message = base64_encode(encrypt($xml));
--------------------encrypt函数--------------------
//DES加密
function encrypt($desStr){
    $key = '你的24位密码';  //注意此为24位密码(对应文档168位对称秘钥)
    $desStr = pkcs5Pad($desStr,8);
    if (strlen($desStr) % 8) {
        $desStr = str_pad($desStr, strlen($desStr) + 8 - strlen($desStr) % 8, "\0");
    }
    $method = 'DES-EDE3';
    return openssl_encrypt($desStr, $method, $key, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING ,'');
}

这里,在php里面直接使用openssl_encrypt可以轻松实现加密,在node中我们使用空字符串作为iv,这里给出具体是实现代码:

let deskey = '我生成的随机24位秘钥'
message = this.desEncrypt(deskey, paramXml);
----------------desEncrypt函数-----------------
    desEncrypt(deskey, xmlData) {
        let secretKey = deskey;
        console.log(secretKey.toString('base64'));
        // properly expand 3DES key from 128 bit to 192 bit
        secretKey = Buffer.from(secretKey);
        const cipher = crypto.createCipheriv('des-ede3', secretKey, '');
        const encrypted = cipher.update(xmlData, 'utf8', 'base64');

        return encrypted + cipher.final('base64');
    }

paramXml这里是将上面sign_code组装后重新进行的一次xml转化。

如果不出意外,主体消息的加密数据我们就完成了,下面进入加密这个24位des加密用的key的流程:

//使用通联的公钥KEY加密对称密钥KEY值,加密算法为RSA/ECB/PKCS1Padding;
$key = encryptByPrivateCerFile('你的24位密码',file_get_contents("../wts.cer"));
----------------encryptByPrivateCerFile函数-------------//公钥加密
function encryptByPrivateCerFile($data,$cerFile)
{
    $crypted = "";
    //公钥加密
    openssl_public_encrypt($data, $crypted, $cerFile);

    return base64_encode($crypted);
}

这里在php中,公钥加密只需要传入.cer文件并使用openssl_public_encrypt函数即可完成加密,咱们来看在node中怎么弄:

// 公钥加密
let encKey = await this.encryptPubKey(deskey, this.rsaPublic)
----------------encryptPubKey------------
    encryptPubKey(deskey, rsaPublic) {
        let nodeRsa = new NodeRSA({ b: 128 });
        nodeRsa.importKey(rsaPublic, 'pkcs8-public');
        nodeRsa.setOptions({ encryptionScheme: 'pkcs1' })
        let data = deskey;
        console.log("encryptPubKey - >", data)
        let encrypted = nodeRsa.encrypt(data, 'base64');
        return encrypted
    }

跟上面一样this.rsaPublic是读取的wts_pub.pem公钥,这里用到了node-rsa库,请自行引用,需要注意的是:new NodeRSA({ b: 128 })中的b: The length of the key in bits,我们使用的是pkcs1的加密模式,所以长度需要给128位,使用这段代码即可完成公钥加密。

最后,将内容组装好后进行一次base64编码:

let newData = '';
newData = '<STSPackage><EncryptedText>';
newData += message + '</EncryptedText>';
newData += '<KeyInfo><ReceiverX509CertSN>';
newData += '1283838383838383838383838';//通联公钥序列号 这里是37位长度的指定序列号,详情查看接口文档
newData += '</ReceiverX509CertSN><EncryptedKey>';
newData += encKey;
newData += '</EncryptedKey></KeyInfo></STSPackage>';

let body = { msgPlain: Buffer.from(newData).toString("base64") };

最后,post到通联:

        let options = {
            method: 'post',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded'
                // 'Content-Type': 'application/json'
            },
            data: qs.stringify(body),
            // data: body,
            url: this.prefix + this.urls[type]
        };
let result = await axios(options);

使用的axios库,请自行添加。

提交后,收到返回数据后,对返回数据进行一次解码和验签,基本上就是将前面的流程反过来走一遍。

先解码整个数据,再将KeyInfo中的EncryptedKey节点拿出来进行一次公钥解密(decryptPubKey):

这时候可以拿到一个buffer的des秘钥,拿到整个秘钥对主要数据进行解密(desDecrypt):

let xmlResult = await this.decryptResult(result.data)
----------------decryptResult--------------
    async decryptResult(data) {
        let deData = Buffer.from(data, 'base64').toString()
        console.log("decryptResult 1", deData)
        let jsonData = await this.parseXML(deData)
        console.log("decryptResult 2", jsonData)
        let encData = jsonData.EncryptedText;
        // 解密
        let encKey = jsonData.KeyInfo.EncryptedKey;
        console.log('decryptResult 3', encKey);
        let decKey = this.decryptPubKey(encKey, this.rsaPrivate)
        console.log('decryptResult 4', decKey);
        let decData = this.desDecrypt(decKey, encData);
        console.log('decryptResult 5 result ->', decData);
        return decData;
    }
--------------decryptPubKey------------------
    decryptPubKey(decdata, privateKey) {
        let nodeRsa = new NodeRSA({ b: 128 });
        nodeRsa.importKey(privateKey, 'pkcs8-private');
        nodeRsa.setOptions({ encryptionScheme: 'pkcs1' })
        let data = nodeRsa.decrypt(decdata);
        console.log("decryptPubKey - >", data)
        return data
    }
--------------desDecrypt---------------------
    desDecrypt(bfkey, decdata) {
        let secretKey = bfkey;
        // secretKey = Buffer.from(secretKey);
        const cipher = crypto.createDecipheriv('des-ede3', secretKey, '');
        const encrypted = cipher.update(decdata, 'base64', 'utf8');
        return encrypted + cipher.final('utf8');
    }

这时候,我们就应该能拿到回复数据了,然后做一个验签:

        // 验签
        let resSignCode = finalJson.head.sign_code;
        resSignCode = resSignCode;
        let newStr = xmlResult.replace(/<sign_code>.+?<\/sign_code>/, '');
        console.info('newStr ', newStr);

        let checkSign = this.sha1signVerfiy(newStr, resSignCode, this.rsaPublic)
        if (!checkSign) {
            throw new Error('check sign fail');
        }
--------------sha1signVerfiy----------------
    sha1signVerfiy(str, sign, pubkey) {
        console.log('>>>>>>>>>>使用 crypto 签名验证>>>>>>>>>>', sign);
        const verify = crypto.createVerify('SHA1');
        // 对具体数据验证
        verify.update(str);
        verify.end();
        const data = verify.verify(pubkey, Buffer.from(sign, "base64"));
        console.log(data);
        return data
    }

就这样,下单的整个流程就结束了,弄完之后发现其实也没那么复杂,主要问题在于对于加密解密算法的认识还不够以至于走了很多岔路。

欢迎大佬围观!

通联支付踩坑实录(nodejs版)
通联支付踩坑实录(nodejs版)
golang后端技术服务器开发

部分代码-学习问题(golang)

2022-5-19 3:56:29

技术

分享一个好用的Nodejs爬虫工具-crawler

2022-5-23 6:01:59

搜索