微信支付-Native下单API支付封装+签名流程解读

简介: 微信支付-Native下单API支付封装+签名流程解读

获取公钥-封装获取微信平台证书列表+解密得到微信公钥

pay.weixin.qq.com/wiki/doc/ap…

pay.weixin.qq.com/wiki/doc/ap…

解密证书列表拿到public key

//解密证书列表 解出CERTIFICATE以及public key
async decodeCertificates() {
  let result = await this.getCertificates();
  if (result.status != 200) {
    throw new Error('获取证书列表失败')
  }
  let certificates = typeof result.data == 'string' ? JSON.parse(result.data).data : result.data.data
  for (let cert of certificates) {
    let output = this.decode(cert.encrypt_certificate)
    cert.decrypt_certificate = output.toString()
    let beginIndex = cert.decrypt_certificate.indexOf('-\n')
    let endIndex = cert.decrypt_certificate.indexOf('\n-')
    let str = cert.decrypt_certificate.substring(beginIndex + 2, endIndex)
    // 生成X.509证书
    let x509Certificate = new x509.X509Certificate(Buffer.from(str, 'base64'));
    let public_key = Buffer.from(x509Certificate.publicKey.rawData).toString('base64')
    // 平台证书公钥
    cert.public_key = `-----BEGIN PUBLIC KEY-----\n` + public_key + `\n-----END PUBLIC KEY-----`
  }
  return this.certificates = certificates
}

其目的是从微信支付平台获取到商户证书列表,然后解析各个证书,并将关键信息存储到 certificates 数组中。

具体如下:

  1. 首先,在方法内部调用了当前对象上的 getCertificates() 方法,异步获取商户证书列表并将结果存储到变量 result 中。
  2. 然后,判断结果状态码是否等于 200,如果不等于则抛出异常,表示获取证书列表失败。
  3. 接着,从 result 中解析出证书列表 certificates
  4. 遍历证书列表 certificates,对于每一个证书,先将其加密字符串解密得到明文证书 decrypt_certificate,然后根据 X.509 标准生成 X.509 证书,并从中获取公钥,将其转换为 Base64 编码的形式,存储到该证书对象的 public_key 属性中。
  5. 最终将解析后的证书列表 certificates 存储到当前对象的 certificates 属性中,并返回该属性值。

具体解密证书的decode方法

    //解密
    decode(params) {
        const AUTH_KEY_LENGTH = 16;
        // ciphertext = 密文,associated_data = 填充内容, nonce = 位移
        const { ciphertext, associated_data, nonce } = params;
        // 密钥
        const key_bytes = Buffer.from(this.apiv3_private_key, 'utf8');
        // 位移
        const nonce_bytes = Buffer.from(nonce, 'utf8');
        // 填充内容
        const associated_data_bytes = Buffer.from(associated_data, 'utf8');
        // 密文Buffer
        const ciphertext_bytes = Buffer.from(ciphertext, 'base64');
        // 计算减去16位长度
        const cipherdata_length = ciphertext_bytes.length - AUTH_KEY_LENGTH;
        // upodata
        const cipherdata_bytes = ciphertext_bytes.slice(0, cipherdata_length);
        // tag
        const auth_tag_bytes = ciphertext_bytes.slice(cipherdata_length, ciphertext_bytes.length);
        const decipher = crypto.createDecipheriv(
            'aes-256-gcm', key_bytes, nonce_bytes
        );
        decipher.setAuthTag(auth_tag_bytes);
        decipher.setAAD(Buffer.from(associated_data_bytes));
        const output = Buffer.concat([
            decipher.update(cipherdata_bytes),
            decipher.final(),
        ]);
        return output;
    }

具体如下:

  1. 首先,该方法接受一个 params 对象,包含需要解密的密文 ciphertext、填充内容 associated_data 和位移 nonce
  2. 然后,从当前对象的 apiv3_private_key 属性中获取到密钥字符串 key_bytes
  3. 接着,将 nonceassociated_data 转换为 Buffer 类型的对象 nonce_bytesassociated_data_bytes
  4. ciphertext 转换成 base64 编码的 Buffer 类型的对象 ciphertext_bytes
  5. 根据算法规范,计算去掉认证信息长度的密文长度。
  6. 将密文截取得到明文部分 cipherdata_bytes 和认证部分 auth_tag_bytes
  7. 使用 Node.js 自带的 crypto 模块创建一个 Decipheriv 类实例,使用 aes-256-gcm 算法解密密文。同时设置认证标签和填充内容,用于验证密文的完整性和正确性。
  8. 最后调用 decipher.update() 方法将密文进行解密,返回解密后的 Buffer 类型的对象 output,并将其返回。

完整代码

const urllib = require('urllib');
const { KJUR, hextob64 } = require('jsrsasign')
const RandomTool = require('./RandomTool')
const crypto = require("crypto");
const x509 = require('@peculiar/x509');
class WxPayment {
    constructor({ appid, mchid, private_key, serial_no, apiv3_private_key, notify_url } = {}) {
        this.appid = appid; // 公众号appid
        this.mchid = mchid; // 商户号mchid
        this.private_key = private_key; // 商户私钥
        this.serial_no = serial_no; // 证书序列号,用于声明所使用的证书
        this.apiv3_private_key = apiv3_private_key; // APIv3密钥,解密平台证书
        this.notify_url = notify_url; // 回调地址
        this.requestUrls = {
            // pc端native下单API
            native: () => {
                return {
                    url: 'https://api.mch.weixin.qq.com/v3/pay/transactions/native',
                    method: 'POST',
                    pathname: '/v3/pay/transactions/native',
                }
            },
            // 获取平台证书url
            getCertificates: () => {
                return {
                    url: `https://api.mch.weixin.qq.com/v3/certificates`,
                    method: `GET`,
                    pathname: `/v3/certificates`,
                }
            },
            // 通过out_trade_no查询订单url配置
            getTransactionsByOutTradeNo: ({ pathParams }) => {
                return {
                    url: `https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/${pathParams.out_trade_no}?mchid=${this.mchid}`,
                    method: `GET`,
                    pathname: `/v3/pay/transactions/out-trade-no/${pathParams.out_trade_no}?mchid=${this.mchid}`,
                }
            },
        }
        // 初始化平台证书
        this.decodeCertificates()
    }
    // 通过商户订单号out_trade_no查询订单
    async getTransactionsByOutTradeNo(params) {
        return await this.wxSignRequest({ pathParams: params, type: 'getTransactionsByOutTradeNo' })
    }
    // 请求微信服务器签名封装
    async wxSignRequest({ pathParams, bodyParams, type }) {
        let { url, method, pathname } = this.requestUrls[type]({ pathParams })
        let timestamp = Math.floor(Date.now() / 1000) // 时间戳
        let onece_str = RandomTool.randomString(32);  // 随机串 
        let bodyParamsStr = bodyParams && Object.keys(bodyParams).length ? JSON.stringify(bodyParams) : '' // 请求报文主体
        let signature = this.rsaSign(`${method}\n${pathname}\n${timestamp}\n${onece_str}\n${bodyParamsStr}\n`, this.private_key, 'SHA256withRSA')
        // 请求头传递签名
        let Authorization = `WECHATPAY2-SHA256-RSA2048 mchid="${this.mchid}",nonce_str="${onece_str}",timestamp="${timestamp}",signature="${signature}",serial_no="${this.serial_no}"`
        // 接口请求
        let { status, data } = await urllib.request(url, {
            method: method,
            dataType: 'text',
            data: method == 'GET' ? '' : bodyParams,
            timeout: [10000, 15000],
            headers: {
                'Content-Type': 'application/json',
                'Accept': 'application/json',
                'Authorization': Authorization
            },
        })
        return { status, data }
    }
    //native统一下单
    async native(params) {
        let bodyParams = {
            ...params,
            appid: this.appid,
            mchid: this.mchid,
            notify_url: this.notify_url,
        }
        return await this.wxSignRequest({ bodyParams, type: 'native' })
    }
    // 获取平台证书
    async getCertificates() {
        return await this.wxSignRequest({ type: 'getCertificates' })
    }
    /**
     * rsa签名
     * @param content 签名内容
     * @param privateKey 私钥,PKCS#1
     * @param hash hash算法,SHA256withRSA
     * @returns 返回签名字符串,base64
     */
    rsaSign(content, privateKey, hash = 'SHA256withRSA') {
        // 创建 Signature 对象
        const signature = new KJUR.crypto.Signature({
            alg: hash,
            // 私钥
            prvkeypem: privateKey
        })
        // 传入待加密字符串
        signature.updateString(content)
        // 生成密文
        const signData = signature.sign()
        // 将内容转成base64
        return hextob64(signData)
    }
    //验证签名 timestamp,nonce,serial,signature均在HTTP头中获取,body为请求参数
    async verifySign({ timestamp, nonce, serial, body, signature }) {
        // 拼接参数
        let data = `${timestamp}\n${nonce}\n${typeof body == 'string' ? body : JSON.stringify(body)}\n`;
        // 用crypto模块解密
        let verify = crypto.createVerify('RSA-SHA256');
        // 添加摘要内容
        verify.update(Buffer.from(data));
        // 从初始化的平台证书中获取公钥
        for (let cert of this.certificates) {
            if (cert.serial_no == serial) {
                return verify.verify(cert.public_key, signature, 'base64');
            } else {
                throw new Error('平台证书序列号不相符')
            }
        }
    }
    //解密证书列表 解出CERTIFICATE以及public key
    async decodeCertificates() {
        let result = await this.getCertificates();
        if (result.status != 200) {
            throw new Error('获取证书列表失败')
        }
        let certificates = typeof result.data == 'string' ? JSON.parse(result.data).data : result.data.data
        for (let cert of certificates) {
            let output = this.decode(cert.encrypt_certificate)
            cert.decrypt_certificate = output.toString()
            let beginIndex = cert.decrypt_certificate.indexOf('-\n')
            let endIndex = cert.decrypt_certificate.indexOf('\n-')
            let str = cert.decrypt_certificate.substring(beginIndex + 2, endIndex)
            // 生成X.509证书
            let x509Certificate = new x509.X509Certificate(Buffer.from(str, 'base64'));
            let public_key = Buffer.from(x509Certificate.publicKey.rawData).toString('base64')
            // 平台证书公钥
            cert.public_key = `-----BEGIN PUBLIC KEY-----\n` + public_key + `\n-----END PUBLIC KEY-----`
        }
        return this.certificates = certificates
    }
    //解密
    decode(params) {
        const AUTH_KEY_LENGTH = 16;
        // ciphertext = 密文,associated_data = 填充内容, nonce = 位移
        const { ciphertext, associated_data, nonce } = params;
        // 密钥
        const key_bytes = Buffer.from(this.apiv3_private_key, 'utf8');
        // 位移
        const nonce_bytes = Buffer.from(nonce, 'utf8');
        // 填充内容
        const associated_data_bytes = Buffer.from(associated_data, 'utf8');
        // 密文Buffer
        const ciphertext_bytes = Buffer.from(ciphertext, 'base64');
        // 计算减去16位长度
        const cipherdata_length = ciphertext_bytes.length - AUTH_KEY_LENGTH;
        // upodata
        const cipherdata_bytes = ciphertext_bytes.slice(0, cipherdata_length);
        // tag
        const auth_tag_bytes = ciphertext_bytes.slice(cipherdata_length, ciphertext_bytes.length);
        const decipher = crypto.createDecipheriv(
            'aes-256-gcm', key_bytes, nonce_bytes
        );
        decipher.setAuthTag(auth_tag_bytes);
        decipher.setAAD(Buffer.from(associated_data_bytes));
        const output = Buffer.concat([
            decipher.update(cipherdata_bytes),
            decipher.final(),
        ]);
        return output;
    }
}
module.exports = WxPayment;

代码逻辑:

实例化时调用this.decodeCertificates() 初始化证书,接着调用getCertificates请求平台证书。然后便利平台证书进行解密拿到平台证书公钥。

封装验证微信发起的签名+解密数据得到用户订单信息

开发回调地址接口对应的逻辑

Service

callback: async (req) => {
  let timestamp = req.header('Wechatpay-Timestamp')
  let nonce = req.header('Wechatpay-Nonce')
  let serial = req.header('Wechatpay-Serial')
  let signature = req.header('Wechatpay-Signature')
  let body = req.body
  // 1.校验收到的请求是否微信服务器发起
  let result = await payment.verifySign({
    timestamp: timestamp,
    nonce: nonce,
    serial: serial,
    signature: signature,
    body: body
  })
  if (!result) {
    return
  }
  // 2.解密body中的resource数据,拿到用户订单信息
  let bufferOne = payment.decode(body.resource)
  let json = JSON.parse(bufferOne.toString('utf8'))
  console.log(json)
  return BackCode.buildSuccess()
},

验证签名

    //验证签名 timestamp,nonce,serial,signature均在HTTP头中获取,body为请求参数
    async verifySign({ timestamp, nonce, serial, body, signature }) {
        // 拼接参数
        let data = `${timestamp}\n${nonce}\n${typeof body == 'string' ? body : JSON.stringify(body)}\n`;
        // 用crypto模块解密
        let verify = crypto.createVerify('RSA-SHA256');
        // 添加摘要内容
        verify.update(Buffer.from(data));
        // 从初始化的平台证书中获取公钥
        for (let cert of this.certificates) {
            if (cert.serial_no == serial) {
                return verify.verify(cert.public_key, signature, 'base64');
            } else {
                throw new Error('平台证书序列号不相符')
            }
        }
    }
  1. 该方法接收一个参数对象,包括时间戳 timestamp、随机字符串 nonce、平台证书序列号 serial、请求体 body 和签名 signature
  2. timestampnoncebody 拼接成一个字符串 data,使用 \n 进行分隔符,其中需要注意将 body 转换为字符串形式。
  3. 使用 Node.js 自带的 crypto 模块创建一个 Verify 类实例,使用 RSA-SHA256 签名算法进行签名验证。
  4. data 进行摘要计算,在计算中添加需要验证的数据。
  5. 通过遍历初始化时传入的 certificates 数组,查找到与 serial 相对应的平台证书 cert
  6. 如果找到了相应的证书,则使用 verify.verify() 方法进行签名验证,传入公钥 cert.public_key、签名 signature 和编码方式 base64,返回验证结果。
  7. 如果未找到相应的平台证书,则抛出错误提示平台证书序列号不相符。

需要注意的是,该方法使用 RSA-SHA256 算法进行签名验证,同时还需要从初始化传入的平台证书数组 certificates 中获取证书公钥进行验证,以确保请求的合法性。

查询订单⽀付状态逻辑封装+快速验证

pay.weixin.qq.com/wiki/doc/ap…

商户订单号查询
  • 查询订单逻辑封装
//通过out_trade_no查询订单
async getTransactionsByOutTradeNo(params) {
  return await this.wxSignRequest({ pathParams: params, type: 'getTransactionsByOutTradeNo' })
}

验证订单状态查询

const { payment } = require('./config/wechatPay');
(async () => {
    let wechatOrder = await payment.getTransactionsByOutTradeNo({ out_trade_no: '123456789wqjeqjwdiqhdhqd' })
    console.log(wechatOrder.data)
})()

传入商户订单号,和直连商户号mchid 使用 wxSignRequest进行请求。

目录
相关文章
|
3月前
|
缓存 负载均衡 API
深入解析电商支付API的性能瓶颈与解决方案
在电商快速发展的当下,支付API的性能直接影响用户体验与交易成功率。本文深入分析支付API在高并发下的性能瓶颈,并提供包括CDN加速、异步处理、缓存优化、数据库索引、负载均衡等在内的系统性解决方案,助力开发者提升支付效率,保障交易流畅稳定。
81 1
|
3月前
|
监控 前端开发 安全
如何集成第三方支付API到电商网站
在电商网站中,集成第三方支付API是确保交易安全、提升用户体验的关键步骤。本文详细介绍了从选择支付提供商到上线监控的全流程,涵盖代码示例与实用建议,助您高效实现支付功能。
130 0
|
4月前
|
前端开发 搜索推荐 NoSQL
提升用户体验:电商API如何优化购物车与支付流程
本文探讨电商购物车与支付流程优化,通过API技术提升用户体验。一是购物车原子化操作,如ETag版本控制解决冲突、局部更新减少传输;二是支付流程聚合与降级,包括JWT单次验证、动态支付选项及备用渠道切换;三是实时库存保护,利用分布式锁防止超卖。案例显示,优化后购物车冲突减少92%,支付耗时降至2.1秒,移动端转化率提升18%。API作为体验引擎,未来将与前端深度协同,推动更优购物闭环。
115 1
|
3月前
|
消息中间件 缓存 监控
电商API接口功能全景图:商品、订单、支付、物流如何无缝衔接?
在数字化商业中,API已成为电商核心神经系统。本文详解商品、订单、支付与物流四大模块的API功能,探讨其如何协同构建高效电商闭环,并展望未来技术趋势。
|
10天前
|
移动开发 安全 小程序
淘宝/天猫:使用支付宝API实现多场景支付,覆盖用户偏好
本文详解如何通过支付宝API在淘宝、天猫等平台实现多场景支付,覆盖APP、PC、H5及小程序,结合用户偏好动态配置分期、快捷支付等功能,提升转化率与体验。内容涵盖API核心功能、技术示例(Python)、安全实践与性能优化,确保开发高效可靠。
197 3
|
10天前
|
供应链 安全 API
唯品会:利用银行转账API实现企业采购对公支付的技术实践
企业采购支付面临合规、效率与对账难题。唯品会通过银行API实现银企直连,构建安全高效对公支付系统,支持ISO 20022标准与多重风控,支付耗时从72小时降至90秒,错误率下降98%,推动供应链数字化升级。(236字)
96 1
|
9天前
|
BI API 网络架构
Lazada:利用本地化支付API支持东南亚主流电子钱包,提升支付成功率
东南亚电商支付碎片化严重,Lazada通过本地化支付API整合主流电子钱包,构建四层技术架构,实现“一次对接,全域覆盖”。覆盖87种错误场景,支持动态路由与合规校验,支付成功率提升至91.7%,新用户转化率增长56.2%。
65 0
|
10天前
|
安全 NoSQL API
拼多多:通过微信支付API实现社交裂变付款的技术解析
基于微信JSAPI构建社交裂变支付系统,用户发起拼单后生成预订单与分享链接,好友代付后通过回调更新订单并触发奖励。集成微信支付、异步处理、签名验签与Redis关系绑定,提升支付成功率与裂变系数,实现高效安全的闭环支付。
134 0
|
2月前
|
JSON 缓存 供应链
API 接口驱动 1688 采购自动化:从商品获取到下单支付的全流程贯通
在B2B电商采购中,1688开放平台通过API实现商品筛选、比价、下单、支付及物流跟踪的全流程自动化,大幅提升采购效率,降低人工成本与错误率。企业可无缝对接ERP系统,实现数据驱动决策,显著优化采购周期、成本与风险管控,助力数字化转型。
|
2月前
|
JSON 自然语言处理 供应链
API接口赋能1688采购全流程:从商品获取到下单支付一键贯通
1688采购API助力企业实现全流程自动化,涵盖商品数据获取、智能比价、一键下单、支付及物流跟踪等环节,显著提升采购效率,降低成本与风险,推动B2B采购模式智能化升级。

热门文章

最新文章