Bootstrap

node 对接微信支付的踩坑记录(服务端)

因项目需要,对接了微信支付,微信支付对于网页来说没有什么工作量,申请了商户号后,直接将收款码放到网页上就可以,但是小程序需要调起微信支付直接付款,于是认真翻阅了官网要针对小程序做微信支付的对接。

准备工作

按照微信官网,准备以下材料:

  1. 微信商户号
  2. 申请商户api证书
  3. 商户证书序列号
  4. 设置api v3的密钥
  5. 一个关联好相关商户的小程序

流程

根据官网要求小程序支付的流程应该是先调用下单接口,然后再调用调起支付,再接收微信支付通知,获取支付状态。如果有其他原因未完成支付,要调用关闭订单接口。
这里主要写服务端的主要工作流程。

签名

调用微信接口就不能没有签名,这里就不得不骂一句微信,在小程序支付文档的接口中没有明确写明需要签名,只在错误代码中提了签名错误,真的是很害怕人一下就看到要签名吗。在官网开发指南-签名生成可以查看到签名生成的具体步骤,但是因为我的项目使用的是node做服务端,并不能使用样例代码,也不能使用sdk,只能自己写了。

  1. 要加载商户api证书的私钥,使用sha256withrsa算法加密。
// 读取证书私钥
var getKey = function (){
    let path = './apiclient_key.pem';
    let data = fs.readFileSync(path, 'utf-8');
    return  data;
};
// 计算签名
var sign = function (hashType,content) {
    // 将字符串进行utf-8编码
    let string = new Buffer(content);
    let stringUtf8 = string.toString('utf-8');

    privateKey = getKey();

    // 获取加密方式
     let hash = HashMap[hashType]
    // 创建 Signature 对象
    const signature = new KJUR.crypto.Signature({
        alg: hash,
        //!这里指定 私钥 pem!
        prvkeypem: privateKey
    })
    signature.updateString(stringUtf8)
    const signData = signature.sign()
    // 将内容转成base64
    return hextob64(signData);
}

这里使用了第三方插件 KJUR来进行具体的签名算法,这步需要注意的点有以下几种:

  • 将明文加密的第一步,一定要先将明文进行utf-8编码,这部分只在官网给出的样例代码中有,并没有在说明中指出,一开始我就忽略了这一点。
  • 一定要下载官网sdk工具中的验签工具,这个工具会给出相对与返回信息具体的多的签名错误,当然也不是完全的,但大部部分都是有的。
  • 按照官网所说,将签名和签名信息放置在http请求头Authorization属性中时,一定要在使用引号的地方使用双引号,我反复验证了我的签名,验证工具也验证通过了,最后抱着试试的心态,换了下引号,就完全好用了,习惯使用node的话一定要注意这里的单引号和双引号的区别。

验签

服务端对接微信支付的流程工作除了生成签名就是验签了,应微信官网要求,支付通知请求必须要验证他的签名。验签需要api v3密钥,这个是直接在微信商户设置的。

获取平台证书公钥

我们要想验签,按照官网流程,我们首先要获取微信平台证书并导出公钥。使用公钥来进行验签。
请求平台证书的请求也要添加上述签名。
获取到证书后,就涉及到从中读取公钥,并且设置为之后可读,不要每次都去请求证书。同时平台证书有有效期希望通过代码来实现平滑自动更换相关的公钥。
使用了crypto模块的下述方法,但不幸的是,由于我们项目的node版本过低,无法使用这个方法,于是只能放弃自动存入,改为解析后手动导出公钥,以后验签使用已经存储好的公钥文件。

crypto.createPublicKey(decoded)

手动提取公钥的命令

openssl x509 -in public_key.pem -pubkey -noout > public.pem

解密获取证书

另外想要拿到证书文件必须要做的就是解密,微信给出的响应都是加密的,要按照微信要求去解密。,解密方法如下:

	 // 将base64编码解码
    let ciphertext = Buffer.from(string, 'base64');
    let key = setting.weixinPay.api3;
    // 解密证书
    let authTag = ciphertext.slice(ciphertext.length - 16);
    let data = ciphertext.slice(0, ciphertext.length - 16);
    let decipher = crypto.createDecipheriv('aes-256-gcm', key, nonce);
    decipher.setAuthTag(Buffer.from(authTag));
    decipher.setAAD(Buffer.from(associated_data));
    let decoded = decipher.update(data, null, 'utf8');
    try {
      decipher.final();
    } catch (error) {
      console.log(error);
    }
    logUtil.error('decoded: ' + decoded);
  • 上述代码中try catch部分是有一个报错的,但是没有明白为什么报错,同时,也能在上步直接获取解密的信息,所以此处就忽略了它,如果不加这个final的话,解密文件后面会有一部分乱码,所以还是保留了这句,不处理他的错误信息。
  • 另外伤处代码解析的证书,我存储成文件了,后来通过命令行导出证书的公钥,并存为文件。

验签

根据官网要求生成验签串,并获取签名,进行验签。

var verify = function(hashType,pv,str,sign){
    // 获取加密方式
    let hash = HashMap[hashType]
    try {
      let signatureVf = new KJUR.crypto.Signature({alg:hash,prvkeypem:pv});
      signatureVf.updateString(str);
      // 验签入参是16进制字符串,注意转码
      let b = signatureVf.verify(b64tohex(sign));
      return b;
    } catch (error) {
      logUtil.error(error);
    }
    
  }
  • 获取的请求体一定是object,要想征程生成验签名串,要将其转化成字符串使用

验签过后,就可以解密请求主体,获取参数,进行业务逻辑了。解密方法和上述解密证书是相同方法,就不再重复放,只是要注意以下内容。

  • 使用express框架的时候,接收post参数不能简单的使用req.body,要使用let form = new formidable.IncomingForm();来接收参数,如果不成功接收到请求主体,那么验签是不能过的。
;