nodejs整合银联网关支付

银联支付的测试开发做的很完善,可以下载各个语言的测试包,进行开发测试,但是并没有 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 ,生产环境中除非是从商户提交审核的域名发起的请求,否则一律会报错 亲爱的用户,您本次交易可能存在风险.

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值