如题,支付宝官方应该是提供了老版本的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命令执行代码文件,就会看到输出的结果。