NodeJS 支付宝当面付对接

如题,支付宝官方应该是提供了老版本的demo,不过身为强迫症,我不想要如此多的依赖,于是,只好自己造轮子了。

API中最难的部分应该是签名了,在下面的代码中已经实现了,直接用就可以。

以下代码包含了创建预收款账单(收款二维码)撤销账单(完全退款,关闭账单),以及退款(部分 / 完全退款),三个最常用的接口;以及,API验签(同步验签)服务端验签(异步验签)等部分;甚至,包括了当证书过期之后的自动更新。

例子中的接口参数并不完全,有些可选参数可以自己添加,按照代码中的模式传入参数即可,调用时会自动签名。

注意,以下代码均为证书模式,不是普通公钥模式。
调用的第三方依赖只有两个,“axios”和“@fidm/x509”。

安装这两个依赖:
npm i -S axios
npm i -S @fidm/x509

下面贴代码。

const crypto = require('crypto');
const axios = require('axios');
const x509 = require('@fidm/x509');
const fs = require('fs');

const callback = '';
 // 接收收款信息的回调地址
const appId = ''; // 支付宝的APPID
const gateway = 'https://openapi.alipay.com/gateway.do';
const private = fs.readFileSync('./rsa/private.key').toString();
// 上面一项是支付宝开发工具生成的私钥,文件名为“xxx_私钥.txt”
// 在开头加上“-----BEGIN RSA PRIVATE KEY-----”
// 在结尾加上“-----END RSA PRIVATE KEY-----”
const appCertSN = getAppCertSign(fs.readFileSync('./rsa/appCert.crt'));
// 这里对应“appCertPublicKey_xxx.crt”
var aliCertSN = getAppCertSign(fs.readFileSync('./rsa/aliCert.crt'));
// 这里对应“alipayCertPublicKey_RSA2.crt”
var aliPubKey = getKeyFromCert(fs.readFileSync('./rsa/aliCert.crt'));
// 同上,对应“alipayCertPublicKey_RSA2.crt”
const rootCertSN = getRootCertSign(fs.readFileSync('./rsa/aliRootCert.crt'));
// 这里对应“alipayRootCert.crt”

const emptyFunc = function () {}; // 一个普通的空函数

createOrder((new Date()).valueOf(), 1.88, '收款收款,这是标题', function (res, err) {
  if (err !== undefined) {
    console.log('Error Occured: ' + err);
  }
  console.log(res);
  // res.qr_code就是收款二维码的链接,转换成二维码即可
});

// 预创建收款账单
function createOrder(order, price, title, callback) {
  let params = getQueryParams('alipay.trade.precreate', {
    out_trade_no: order,
    total_amount: price,
    subject: title
  });
  axios.get(gateway, {params: params}).then(async function (res) {
    let body = res.data;
    let data = body.alipay_trade_precreate_response;
    if (callback) {
      if (body.alipay_cert_sn !== aliCertSN) {
        await getLatestAliCert();
      }
      if (!syncCheckV2(body)) {
        callback(data, 1);
      } else {
        if (data.code != '10000') {
          callback(data, 2);
        } else {
          callback(data);
        }
      }
    }
  }).catch(emptyFunc);
}
// 撤销账单,注意,是撤销账单,会退钱的!
function cancelOrder(order, callback) {
  let params = getQueryParams('alipay.trade.cancel', { out_trade_no: order });
  axios.get(gateway, {params: params}).then(async function (res) {
    let body = res.data;
    let data = body.alipay_trade_cancel_response;
    if (callback) {
      if (body.alipay_cert_sn !== aliCertSN) {
        await getLatestAliCert();
      }
      if (!syncCheckV2(body)) {
        callback(data, 1);
      } else {
        if (data.code != '10000') {
          callback(data, 2);
        } else {
          callback(data);
        }
      }
    }
  }).catch(emptyFunc);
}
// 退款,参数分别是:商家端的订单号,回调函数,退款金额,退款原因,退款请求的编号
// 注:退款请求的编号一般可以不填,但是要分批退款的时候必填
function refundOrder(order, callback, amount, reason, reqNo) {
  let params = { out_trade_no: order };
  if (!isNaN(parseFloat(amount))) params.refund_amount = amount;
  if (typeof reason === 'string') params.refund_reason = reason;
  if (typeof reqNo === 'string') params.out_request_no = reqNo;
  params = getQueryParams('alipay.trade.refund', params);
  axios.get(gateway, {params: params}).then(async function (res) {
    let body = res.data;
    let data = body.alipay_trade_refund_response;
    if (callback) {
      if (body.alipay_cert_sn !== aliCertSN) {
        await getLatestAliCert();
      }
      if (!syncCheckV2(body)) {
        callback(data, 1);
      } else {
        if (data.code != '10000') {
          callback(data, 2);
        } else {
          callback(data);
        }
      }
    }
  }).catch(emptyFunc);
}

// 生成公共参数,以及签名
function getQueryParams(api, params) {
  let pubArgs = {
    app_id: appId,
    method: api,
    format: 'JSON',
    charset: 'UTF-8',
    sign_type: 'RSA2',
    timestamp: getDate(new Date()),
    version: '1.0',
    notify_url: callback,
    biz_content: JSON.stringify(params),
    app_cert_sn: appCertSN,
    alipay_root_cert_sn: rootCertSN
  };
  let str = '';
  let ordered = Object.keys(pubArgs).sort();
  for (let i = 0; i < ordered.length; i++) {
    let k = ordered[i];
    str += k + '=' + pubArgs[k] + '&';
  }
  str = str.slice(0, -1);
  pubArgs.sign = crypto.createSign('RSA-SHA256').update(str, 'utf8').sign(private, 'base64');
  return pubArgs;

  function getDate(date) {
    return date.getFullYear()+'-'+strFill(date.getMonth()+1)+'-'+strFill(date.getDate())+' '+strFill(date.getHours())+':'+strFill(date.getMinutes())+':'+strFill(date.getSeconds());
    function strFill (str) { return ('0'+str).substr(-2); }
  }
}
// 从阿里的证书中获取公钥,用来验签
function getKeyFromCert(pem) {
  return '-----BEGIN PUBLIC KEY-----\r\n' + x509.Certificate.fromPEM(pem).publicKeyRaw.toString('base64') + '\r\n-----END PUBLIC KEY-----';
}
// 获取证书的Sign
function getAppCertSign(pem) {
  return getCertSign(x509.Certificate.fromPEM(pem));
}
// 获取支付宝根证书的Sign
function getRootCertSign(pem) {
  let certs = x509.Certificate.fromPEMs(pem);
  let rootCertSN = '';
  certs.forEach((item) => {
    if (item.signatureOID.startsWith('1.2.840.113549.1.1')) {
      let SN = getCertSign(item);
      if (rootCertSN.length === 0) {
        rootCertSN += SN;
      } else {
        rootCertSN += '_' + SN;
      }
    }
  });
  return rootCertSN;
}
// 获取证书的Sign
function getCertSign(cert) {
  let serialNumber = cert.serialNumber;
  let principalName = cert.issuer.attributes
  .reduceRight((prev, curr) => prev + curr.shortName + '=' + curr.value + ',', '')
  .slice(0, -1);
  return crypto.createHash('md5').update(principalName + hexToDec(serialNumber), 'utf8').digest('hex');
  function hexToDec(s) {
    let i, j, digits = [0], carry;
    for (i = 0; i < s.length; i += 1) {
      carry = parseInt(s.charAt(i), 16);
      for (j = 0; j < digits.length; j += 1) {
        digits[j] = digits[j] * 16 + carry;
        carry = digits[j] / 10 | 0;
        digits[j] %= 10;
      }
      while (carry > 0) {
        digits.push(carry % 10);
        carry = carry / 10 | 0;
      }
    }
    return digits.reverse().join('');
  }
}

// 同步验签,即请求支付宝接口后验证是否为支付宝官方的响应
function syncCheckV2(body) {
  let key = null;
  if (!('sign' in body)) return false;
  for (let k in body) {
    if (k.endsWith('_response')) {
      key = k;
      break;
    }
  }
  if (key === null) return false;
  let signStr = JSON.stringify(body[key]).replace(/\//g, '\\/');
  return crypto.createVerify('RSA-SHA256').update(signStr, 'utf8').verify(aliPubKey, body.sign, 'base64');
}
// 异步验签,用在服务器上,验证回调地址收到的数据是否来自支付宝
function asyncCheckV2(body) {
  let temp = Object.assign({}, body);
  console.log(temp);
  let sign = temp.sign;
  delete temp.sign;
  delete temp.sign_type;
  let str = '';
  let keys = Object.keys(temp).sort();
  for (let i = 0; i < keys.length; i++) {
    let k = keys[i];
    str += k + '=' + decodeURIComponent(temp[k]) + '&';
  }
  str = str.slice(0, -1);
  return crypto.createVerify('RSA-SHA256').update(str).verify(aliPubKey, sign, 'base64');
}
// 当本地的支付宝证书过期后,自动从官方更新
async function getLatestAliCert(certSN) {
  let params = getQueryParams('alipay.open.app.alipaycert.download', { alipay_cert_sn: certSN });
  await axios.get(gateway, {params: params}).then(res => {
    let cert = Buffer.from(res.data.alipay_open_app_alipaycert_download_response.alipay_cert_content, 'base64').toString('utf-8');
    fs.writeFileSync('./rsa/aliCert.crt', cert);
    aliCertSN = getAppCertSign(cert);
    aliPubKey = getKeyFromCert(cert);
  }).catch(emptyFunc);
}

用法:准备好应用私钥(开发助手生成),应用公钥证书,支付宝公钥证书,支付宝根证书,然后修改代码中的路径,再修改代码中有关APPID和回调的部分。

运行:cd到代码文件的目录下,用node命令执行代码文件,就会看到输出的结果。

上一篇
下一篇