node.js开发微信与支付宝的手机网站支付

之前并没有用node.js做过H5相关的支付开发,查询了很多的资料,自己做一个总结,写了一点儿demo。

如果有问题,感谢指出。

参考博客:https://www.jianshu.com/p/10089108da21

一、微信的手机网站支付开发

H5的微信支付相关的开发,比微信公众号内的支付要相对简单一些。

微信h5支付开发的文档地址:https://pay.weixin.qq.com/wiki/doc/api/H5.php?chapter=15_1

首先要签名相关的通用方法,签名的逻辑https://pay.weixin.qq.com/wiki/doc/api/H5.php?chapter=4_3

我对于签名的代码实现如下:

'use strict';

const crypto = require('crypto');

/**
 * 构建参数
 */
function composeParam(params)
{
    let signedData = getSignedData(params);
    if(params){
        signedData = signature(signedData);
    }
    if(signedData){
        params.sign = signedData;
    }

    return composeXml(params);
} 

/**
 * 构建xml
 */
function composeXml(params)
{
	let result = '';
    for(let key in params){
        if(params.hasOwnProperty(key)){
            result += '<' + key +'>'  + params[key]  + '</' + key + '>';
            // result += '<' + key +'>' + '

对于请求的代码我的实现如下:

const request = require('request');

function sendPostHttpRequest(url, param, callback)
{
    request.post(
 		{
 			url : url, 
 			body: JSON.stringify(param)
 		}, 
 		function (error, response, body) {
 			if(error){
 				return callback(error, null);
 			}
 			callback(null, body);
 		}
 	);
}

解析微信返回的xml数据:

const xml2js = require('xml2js').parseString;
/**
 * xml转对象
 */
function xmlToObject(xml, callback)
{
	xml2js(xml, {explicitArray:false}, function(error, result){
		if (error) {
			return callback(error, null);
		}else{
			callback(null, result);
		}
	})
}

剩下的工作就是跟着文档走了。

不过微信的开发文档在表述上有一些歧义,多注意文档中标红的信息。如果是在手机网站上进行的支付,那么就不需要获取access_token和openid,另外,需要确认你的商户是否有接入H5支付的功能,公众号支付和app支付对应的商户平台是不一样的。

将参数通过请求调用方法,调用微信的统一下单接口,获取的微信的相应数据,如果请求的数据没有问题,微信返回的数据将包含一个支付跳转链接mweb_url,直接跳转到这个链接就可以调起微信支付。

还有一些坑,在调起这个连接时,要保证请求头中refer是有值的。

随后,用户支付后,微信会向回调地址(notify_url)发送一个支付信息:https://pay.weixin.qq.com/wiki/doc/api/H5.php?chapter=9_7&index=8

需要对微信所发的数据进行验签,将除了sign字段的所有参数都拿去签名,如果签名一致,则验签成功。

二、支付宝的手机网站支付开发

支付宝H5开发文档:https://docs.open.alipay.com/203

个人感觉,支付宝的开发相对简单一些。

支付宝的加密方式是sha-256,有两对公私钥。

在本地生成一对密钥(支付宝的文档中有写),将生成的应用公钥保存到支付宝的应用后台,将支付宝提供的公钥保存到本地。

我对于签名的代码实现如下:

const fs = require('fs');
const crypto = require('crypto');

/**
 * 构建 参数
 */
function composeParam(params)
{
    let requestParam = getRequestUrl(params);
    let sign = signature(params);
    let result = requestParam + '&sign_type=RSA2&sign=' + sign;
    return result;
}

/**
 * 构建签名参数
 */
function getSignedData(params) 
{
    let data = [];
    if(!params){
        return null;
    }
    
    /* 参数为空与sign不参与签名 */
    for(let key in params) {
        if((!params[key]) || key === "sign") {
            continue;
        }else{
            data.push([key, params[key]]);
        }
    }

    /* 为参数排序 */
    data = data.sort();

    /* 转成url传值所需 */
    let result = '';
    for(let i = 0; i < data.length; i++) {
        let obj = data[i];
        if(i == data.length - 1) {
            result = result + obj[0] + '=' + obj[1] + '';
        } else {
            result = result + obj[0] + '=' + obj[1] + '&';
        }
    }

    return result;
}

/**
 * 为最后的地址构建参数
 */
function getRequestUrl(params) 
{
    let data = [];
    if(!params){
        return null;
    }
    
    for(let key in params) {
        if((!params[key]) || key === "sign" || key === "sign_type") {
            continue;
        }else{
            data.push([key, params[key]]);
        }
    }

    /* 为参数排序 */
    data = data.sort();

    /* 转成url传值所需 */
    let result = '';
    for(let i = 0; i < data.length; i++) {
        let obj = data[i];
        if(i == data.length - 1) {
            result = result + obj[0] + '=' + encodeURIComponent(obj[1]) + '';
        } else {
            result = result + obj[0] + '=' + encodeURIComponent(obj[1]) + '&';
        }
    }

    return result;
}

/**
 * 签名
 */
function signature(params) 
{
    try {
        /* 读取私钥 */
        let privatePem = fs.readFileSync(__dirname + '/privateKey/app_private_key.pem');
        let key = privatePem.toString();
        let str = getSignedData(params)
        let sign = crypto.createSign('RSA-SHA256');
        sign.update(str);
        sign = sign.sign(key, 'base64');
        
        return encodeURIComponent(sign);

    } catch(err) {
        console.log('签名错误:', err)
    }
}

module.exports.composeParam = composeParam;

将生成的参数,直接挂在请求地址后,用浏览器打开该地址,就可以调起支付宝支付界面。

支付宝跟微信都有一个回调地址(notify_url),需要对请求的数据进行验签。

验证签名代码:

const fs = require('fs');
const crypto = require('crypto');

/**
 * 将参数做urldecode
 */
function decodeVerifyParams(params)
{
    let result = {};
    for(let key in params){ 
        result[key] = params[key];
    }
    return result;
}

/**
 * 获取需要验签的参数
 */
function getVerifyParams(params) 
{
    let verifyParams = [];
    if(!params) return null;
    for(let key in params) {
        if((!params[key]) || key == "sign" || key == "sign_type" || 
        key == "filter_params" || key == "access_token") {
            continue;
        };
        verifyParams.push([key, params[key]]);
    }
    verifyParams = verifyParams.sort();
    let result = '';
    for(let i2 = 0; i2 < verifyParams.length; i2++) {
        let obj = verifyParams[i2];
        if(i2 == verifyParams.length - 1) {
            result = result + obj[0] + '=' + obj[1] + '';
        } else {
            result = result + obj[0] + '=' + obj[1] + '&';
        }
    }
    return result;
}

/**
 * 验证签名
 */
function veriySign(params) 
{
    try {
        let publicPem = fs.readFileSync(__dirname + '/privateKey/app_public_key.pem');
        let publicKey = publicPem.toString();
        let str = getVerifyParams(params);
        var sign = params['sign'] ? params['sign'] : "";
        sign = decodeURIComponent(sign);
        var verify = crypto.createVerify('RSA-SHA256');
        verify.update(str);
        return verify.verify(publicKey, sign, 'base64');

    } catch(err) {
        console.log('验签错误:', err);
    }
}

/**
 * 验证签名
 */
function verifySignature(params)
{
    params = decodeVerifyParams(params);
    return veriySign(params);
}

module.exports.verifySignature = verifySignature;

最后方法会返回true|false,代表验签是否成功。

在对回调地址的数据进行校验时,除了验证签名外,不要忘记对业务数据也进行验证。

demo地址:https://github.com/shen4030/pay

demo仅供参考

发表评论