银联支付的测试开发做的很完善,可以下载各个语言的测试包,进行开发测试,但是并没有 nodejs 的,难点就是证书签名还有验签这两个步骤。
其实银联加密方式和支付宝微信不同的地方在于,使用了非对称加密,意思是为了在网络中传输安全,双方约定各自产生一个公钥还有私钥,私钥自己保存,公钥公开给对方(你要发送信息的人都知道)。当需要传输秘密的信息时候,用自己的私钥加密,发给对方,对方收到信息后,为了判定这个是否伪造(是不是确实从你这儿发送给他的),那么拿出你的公钥进行验证,发现是一样的,那么就可以确定这个确实是你发送的。这样做就可以保证信息的安全。
下面是 code :
银联配置文件:config.js
//配置银联支付需要的数据 - 这都是银联测试商户信息,可以上 https://merchant.unionpay.com/portal/login.jsp 去申请测试商户
merid: '777290058136713', //商户id
font_trans_url: 'https://101.231.204.80:5000/gateway/api/fronttransreq.do', //网关跳转至银联平台支付页面地址
sigle_query_url: 'https://101.231.204.80:5000/gateway/api/querytrans.do', //单笔查询请求地址
sign_cert_dir: __dirname + '/certificates', //签名证书路径
certid: '40220995861346480087409489142384722381',
sign_cert_pwd: '0000000', //签名证书密码
sign_cert_path: __dirname + '/certificates/700000000000001_acp.pfx', //签名用私钥证书
validate_cert_path: __dirname + '/certificates/verify_sign_acp.cer', //验签用银联公钥证书
.pfx 结尾的都是用密码加密过后的私钥
.cer 结尾的都是公钥
银联支付模块 unionpay.js
var validator = require('validator'),
util = require('util'),
_ = require('underscore'),
crypto = require('crypto'),
x509 = require('x509'),
sha1 = require('sha1'),
wopenssl = require('wopenssl'),
config = require('./config'); //加载银联配置
//银联网关支付
var unionpay = {
//创建预订单
/*
* 参数 parms: {out_trade_no: out_trade_no, fee: fee}
* out_trade_no 商户订单号 fee 订单金额,单位分
*/
sdk_front_notice_url: 'http://'+ config.domain +'/unionpay/result', //银联网关支付前台通知地址
sdk_back_notic_url: 'http://'+ config.domain +'/unionpay/productpay', //银联网关支付后台通知地址
createorder:
function(parms, callback) {
var errmsg;
var timestamp = parms.timestamp;
if(parms.paytype==0) {
var back_notic_url = 'http://'+ config.domain +'/unionpay/productpay';
} else if(parms.paytype==1) {
var back_notic_url = 'http://'+ config.domain +'/unionpay/rechargepay';
} else {
var back_notic_url = this.sdk_back_notic_url;
}
var formdata = {
'version' : '5.0.0', //版本号
'encoding' : 'utf-8', //编码方式
'txntype' : '01', //交易类型
'txnsubtype' : '01', //交易子类
'biztype' : '000201', //业务类型
'fronturl' : this.sdk_front_notice_url, //前台通知地址
'signmethod' : '01', //签名方法
'channeltype' : '08', //渠道类型,07-pc,08-手机
'accesstype' : '0', //接入类型
'currencycode' : '156', //交易币种,境内商户固定156
//todo 以下信息需要填写
'merid' : config.merid, //商户代码,请改自己的测试商户号,此处默认取demo演示页面传递的参数
'orderid' : parms.out_trade_no, //商户订单号,8-32位数字字母,不能含“-”或“_”,此处默认取demo演示页面传递的参数,可以自行定制规则
'txntime' : timestamp, //订单发送时间timestamp,格式为yyyymmddhhmmss,取北京时间,此处默认取demo演示页面传递的参数
'txnamt' : parms.fee, //交易金额,单位分
'backurl' : back_notic_url, //后台通知地址
'certid' : '' //可不必填写,在signkeyfrompfx中返回
};
var privatekey;
var certid;
var cert;
signkeyfrompfx(function(err , result){
if (err) {
errmsg = '证书签名失败';
} else {
certid = result.certid;
privatekey = result.key;
formdata.certid = certid;
if (formdata.signature) {
delete formdata.signature
}
//----签名开始----
//参数转变为签名串
var unionpay_parms = transforsign(formdata);
//摘要
var unionpay_parms_sha1 = sha1(unionpay_parms);
//签名
var signer = crypto.createsign('rsa-sha1');
signer.update(unionpay_parms_sha1);
var signature_base64 = signer.sign(privatekey, 'base64');
//放入域中
formdata.signature = signature_base64;
//加入表单请求银联的地址
formdata.action_url = config.font_trans_url;
//console.log(formdata);
if (errmsg) {
callback(errmsg);
} else {
callback(null,formdata);
}
}
});
},
//签名
sign:
function(parms, callback) {
var errmsg;
var formdata = parms;
signkeyfrompfx(function(err , result){
if (err) {
errmsg = '证书签名失败';
} else {
certid = result.certid;
privatekey = result.key;
if (formdata.signature) {
delete formdata.signature
}
//----签名开始----
//参数转变为签名串
var unionpay_parms = transforsign(formdata);
//摘要
var unionpay_parms_sha1 = sha1(unionpay_parms);
//签名
var signer = crypto.createsign('rsa-sha1');
signer.update(unionpay_parms_sha1);
var signature_base64 = signer.sign(privatekey, 'base64');
//放入域中
formdata.signature = signature_base64;
//console.log(formdata);
if (errmsg) {
callback(errmsg);
} else {
callback(null,formdata);
}
}
});
},
//验签
validate:
function(parms,callback) {
var validate_signature = parms.signature;
delete parms.signature;
var formdata = parms;
validatekeyfromcer(formdata,validate_signature,function(err , result){
if (err || !validate_signature || !formdata) {
console.log('验签失败');
callback('验签失败');
} else {
var publickey = result.key;
if (formdata.signature) {
delete formdata.signature
}
//----验签开始----
var unionpay_parms = transforsign(formdata);
var unionpay_parms_sha1 = sha1(unionpay_parms);
//console.log('待验证签:' + validate_signature);
var verifier = crypto.createverify('rsa-sha1');
//console.log('验证签名public key:\n' + publickey);
//console.log('验证签名src_sign:' + unionpay_parms_sha1);
verifier.update(new buffer(unionpay_parms_sha1, 'utf-8'));
var is_success = verifier.verify(publickey, validate_signature, 'base64');
if (is_success) {
callback(null,formdata);
} else {
console.log('验签不相等');
callback('验签不相等');
}
}
});
}
};
// 签名串算法--将参数排序,转成键值对格式字符串
function transforsign(params){
var array = []
for (var i in params) {
array.push('' + i + '=' + params[i])
}
var stringsigntemp = _.sortby(array, function (str) {
return str;
});
return stringsigntemp.join('&');
};
//通过证书密码获得证书的rsa-privatekey值和证书id
function signkeyfrompfx(callback){
if (config.certsdata) {
callback(null, config.certsdata);
} else {
var certpath = config.sign_cert_path;
var certpwd = config.sign_cert_pwd;
var certdir = config.sign_cert_dir;
var p12 = wopenssl.pkcs12.extract(certpath, certpwd);
//console.log(p12.certificate); //p12.certificate和p12.rsa
var certs = wopenssl.x509.parsecert(p12.certificate);
//因为不知道怎么将十六进制证书id:certs.serial变成十进制证书id,因为这是个很大的整形biglong
var certsdata = {};
certsdata.certid = config.certid;
certsdata.key = p12.rsa;
certsdata.ca = certs;
//存入config
config.certsdata = certsdata;
callback(null,certsdata); //{key: string, certid: string, ca: array}
}
};
//获得验签证书的rsa-publickey值
function validatekeyfromcer(formdata, signature, callback){
if (config.validcertsdata) {
callback(null, config.validcertsdata);
} else {
var validatecertpath = config.validate_cert_path;
var certs = wopenssl.x509.parsecert(validatecertpath);
//console.log(certs);
var fs = require('fs');
var certificate = fs.readfilesync(validatecertpath);
console.log(certificate);
var publickey = certificate.tostring('ascii');
var validcertsdata = {};
validcertsdata.key = publickey;
validcertsdata.cert = certificate;
config.validcertsdata = validcertsdata;
if (publickey) {
callback(null,validcertsdata);
} else {
msg = '验签失败';
callback(msg);
}
}
};
//转化时间格式函数
function format(){
//时间格式化
var format = 'yyyymmddhhmmss';
date = new date();
var o = {
'm+' : date.getmonth() + 1, //month
'd+' : date.getdate(), //day
'h+' : date.gethours(), //hour
'm+' : date.getminutes(), //minute
's+' : date.getseconds(), //second
'q+' : math.floor((date.getmonth() + 3) / 3), //quarter
's' : date.getmilliseconds() //millisecond
};
if (/(y+)/.test(format))
format = format.replace(regexp.$1, (date.getfullyear() + '').substr(4 - regexp.$1.length));
for (var k in o)
if (new regexp('(' + k + ')').test(format))
format = format.replace(regexp.$1, regexp.$1.length == 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length));
return format;
};
module.exports = unionpay;
其实最重要的是签名还有验签部分,对证书 .pfx 和 .cer 的处理,其中的 createorder 方法只是方便使用返回的请求表单内容。
在测试完成后,生产环境的配置还是不太一样。当银联商户申请成功后,银联会发一份邮件,上面有商户号,你的私钥证书 .pfx 需要你自己根据他的邮件提示去下载的,附件内的 “证书下载,安装” 文件有详细的说明教程。还有就是配置中的请求地址 https://101.231.204.80:5000需要换为 https://gateway.95516.com ,生产环境中除非是从商户提交审核的域名发起的请求,否则一律会报错 亲爱的用户,您本次交易可能存在风险.