前情提要:最新一个项目需要对接通联支付,对方给过来的接口文档特别繁琐,还有一个实例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版)](https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fwww.linuxprobe.com%2Fwp-content%2Fuploads%2F2017%2F10%2Fnodejs.jpg&refer=http%3A%2F%2Fwww.linuxprobe.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1655769012&t=052feb28f591eda3e0d7f7f8c513e93e)
![通联支付踩坑实录(nodejs版)](https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fmy.yingjiesheng.com%2Fupload%2F1501%2F7812951_1421243423.jpg&refer=http%3A%2F%2Fmy.yingjiesheng.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1655769042&t=f074f19d2dbed036599f471152a37120)