Skip to content
On this page

key排序算法

typescript
const obj = {}
const res = Object.keys(obj).sort().reduce((pre,cur)=>({..pre,[cur]:obj[cur]}))

md5加密

typescript
cosnt crypto = require('crypto-js')
cosnt signResult = crypto.createHash('MD5')
  .update(qs.stringify(sortedParams,{encode:false}))
  .digest("hex").toUpperCase();
return signResult

拓展:qs.stringify拼接对象

typescript
const qs = require("qs");
const obj = { a: 1, b: { c: { d: 1 } } };
console.log(qs.stringify(obj)); // a=1&b%5Bc%5D%5Bd%5D=1
console.log(qs.stringify(obj, { encode: false })); // a=1&b[c][d]=1

步骤流程

参考 https://www.bilibili.com/video/BV12w411o7wJ?p=2&vd_source=11e14f37a256537712e73b4b7f52411c

小程序wx.login获取code 小程序发送code到后端, image.png 后端拿到code,向wx获取数据(session_key,openid), ps:session_key 服务端使用,openid客户端使用 需要参数appid,secret,js_code,grant_type(固定为authorization_code) image.png 后端返回openid,小程序把openid存起来 image.png 小程序开始支付 image.png

先调后端接口,后端去下单,小程序再发起请求

image.png 后台生成预支付订单 image.png

guid --全局唯一id,也是全宇宙唯一id,一共128位

typescript
let guid = require("guid")
module.exports = {
  getGuid(){
    return guid.create().value.replace(/-/g,'')
  }
}

这里后台会报headers athorization的问题 因为要求请求头需要Authorization字段,value也有要求

准备Authorization

生成签名--signature

https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml

文档给的get只是例子,实际还是我们原来的那个请求,也就是之前说headers有问题那个post请求 注意:随机字符串、时间戳前后需要是同一个值,就是和请求params里的得一一对应

typescript
// 格式
HTTP请求方法\n
URL\n
请求时间戳\n
请求随机串\n
请求报文主体\n

// 请求时间戳最好与请求参数里的那个一样

`post\n
/v3/pay/transactions/jsapi\n
${Math.round(+new Date() / 1000)}\n
${JSON.stringify(params)}\n
`
// 上面就是签名串

计算签名值

对签名串进行私钥签名再进行base64编码,最后结果就是Authorization 的signature

私钥签名在编码的详细操作

java php 等提供了相应方法,nodejs则没有 步骤是 sha256 with rsa 然后 base64编码 https://developers.weixin.qq.com/community/pay/doc/000ae48c5b0c709d938bdc46e56c00?jumpto=comment&commentid=0004ae8eb20ea0bf948b3a29a5fc 大神封装

typescript
// https://github.com/TheNorthMemory/wechatpay-axios-plugin/blob/master/lib/rsa.js
const {
  publicEncrypt, privateDecrypt, createSign, createVerify,
  constants: { RSA_PKCS1_PADDING, RSA_PKCS1_OAEP_PADDING },
} = require('crypto');

/** @constant 'sha1' */
const sha1 = 'sha1';
/** @constant 'utf8' */
const utf8 = 'utf8';
/** @constant 'base64' */
const base64 = 'base64';
/** @constant 'sha256WithRSAEncryption' */
const sha256WithRSAEncryption = 'sha256WithRSAEncryption';

/**
 * @param {number} code - Supporting `RSA_PKCS1_OAEP_PADDING` or `RSA_PKCS1_PADDING`, default is `RSA_PKCS1_OAEP_PADDING`.
 * @throws {RangeError} - While the padding isn't `RSA_PKCS1_OAEP_PADDING` nor `RSA_PKCS1_PADDING`.
 * @returns {void}
 */
const paddingModeLimitedCheck = (code) => {
  if (!(code === RSA_PKCS1_PADDING || code === RSA_PKCS1_OAEP_PADDING)) {
    throw new RangeError(`Doesn't supported the padding mode(${code}), here's only support RSA_PKCS1_OAEP_PADDING or RSA_PKCS1_PADDING.`);
  }
};

/**
 * Provides some methods for the RSA `sha256WithRSAEncryption` with `RSA_PKCS1_OAEP_PADDING`.
 */
class Rsa {
  /**
   * Alias of the `RSA_PKCS1_OAEP_PADDING` mode
   */
  static get RSA_PKCS1_OAEP_PADDING() { return RSA_PKCS1_OAEP_PADDING; }

  /**
   * Alias of the `RSA_PKCS1_PADDING` mode
   */
  static get RSA_PKCS1_PADDING() { return RSA_PKCS1_PADDING; }

  /**
   * Encrypts text with sha256WithRSAEncryption/RSA_PKCS1_OAEP_PADDING.
   * Recommended Node Limits Version >= 12.9.0 (`oaepHash` was available), even if it works on v10.15.0.
   *
   * @param {string} plaintext - Cleartext to encode.
   * @param {string|Buffer} publicKey - A PEM encoded public certificate.
   * @param {number} padding - Supporting `RSA_PKCS1_OAEP_PADDING` or `RSA_PKCS1_PADDING`, default is `RSA_PKCS1_OAEP_PADDING`.
   *
   * @returns {string} Base64-encoded ciphertext.
   * @throws {RangeError} - While the padding isn't `RSA_PKCS1_OAEP_PADDING` nor `RSA_PKCS1_PADDING`.
   */
  static encrypt(plaintext, publicKey, padding = RSA_PKCS1_OAEP_PADDING) {
    paddingModeLimitedCheck(padding);
    return publicEncrypt({
      oaepHash: sha1,
      key: publicKey,
      padding,
    }, Buffer.from(plaintext, utf8)).toString(base64);
  }

  /**
   * Decrypts base64 encoded string with `privateKey`.
   * Recommended Node Limits Version >= 12.9.0 (`oaepHash` was available), even if it works on v10.15.0.
   *
   * @param {string} ciphertext - Was previously encrypted string using the corresponding public certificate.
   * @param {string|Buffer} privateKey - A PEM encoded private key certificate.
   * @param {number} padding - Supporting `RSA_PKCS1_OAEP_PADDING` or `RSA_PKCS1_PADDING`, default is `RSA_PKCS1_OAEP_PADDING`.
   *
   * @returns {string} Utf-8 plaintext.
   * @throws {RangeError} - While the padding isn't `RSA_PKCS1_OAEP_PADDING` nor `RSA_PKCS1_PADDING`.
   */
  static decrypt(ciphertext, privateKey, padding = RSA_PKCS1_OAEP_PADDING) {
    paddingModeLimitedCheck(padding);
    return privateDecrypt({
      oaepHash: sha1,
      key: privateKey,
      padding,
    }, Buffer.from(ciphertext, base64)).toString(utf8);
  }

  /**
   * Creates and returns a `Sign` string that uses `sha256WithRSAEncryption`.
   *
   * @param {string|Buffer} message - Content will be `crypto.Sign`.
   * @param {string|Buffer} privateKey - A PEM encoded private key certificate.
   *
   * @returns {string} Base64-encoded signature.
   */
  static sign(message, privateKey) {
    return createSign(sha256WithRSAEncryption).update(message).sign(
      privateKey,
      base64,
    );
  }

  /**
   * Verifying the `message` with given `signature` string that uses `sha256WithRSAEncryption`.
   *
   * @param {string|Buffer} message - Content will be `crypto.Verify`.
   * @param {string} signature - The base64-encoded ciphertext.
   * @param {string|Buffer} publicKey - A PEM encoded public certificate.
   *
   * @returns {boolean} True is passed, false is failed.
   */
  static verify(message, signature, publicKey) {
    return createVerify(sha256WithRSAEncryption).update(message).verify(
      publicKey,
      signature,
      base64,
    );
  }
}

module.exports = Rsa;
module.exports.default = Rsa;

其他方案:网友评论

typescript
const nonceStr = Math.random().toString(36).slice(-10)
const timestamp = (new Date().getTime() / 1000).toFixed(0)
const message = `GET\n/v3/certificates\n${timestamp}\n${nonceStr}\n\n`
const signature = crypto.createSign('RSA-SHA256')
.update(message, 'utf-8')
.sign(fs.readFileSync('./apiclient_key.pem')
.toString(), 'base64')

以上两种可以试一试,哪个靠谱用哪个 以下使用第一种进行演示

typescript
const Rsa = require('./rsa.js);// 就是第一个方案的文件
function calcSign(msg){
  let pem = fs.readFileSync('./apiclient_key.pem')
  return Rsa.sign(msg,pem).toString('base64')
}

验签

利用官方提供的工具可以验证你计算的签名值是否正确 https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtmlimage.png 选择文件 image.png 密码是商户号 image.png 计算前的参数拷贝进去,看官方工具计算结果和你的一不一样 image.pngimage.png

设置Athorization

image.png 所以后端的headers最后设置成 注意:保留双引号的

typescript
axios.post('xx',[params],{
  headers:{'Authorization':`WECHATPAY2-SHA256-RSA2048 mchid="1900009191",nonce_str="593BEC0C930BF1AFEB40B4A08C8FB242",signature="uOVRnA4qG/MNnYzdQxJanN+zU+lTgIcnU9BxGw5dKjK+VdEUz2FeIoC+D5sB/LN+nGzX3hfZg6r5wT1pl2ZobmIc6p0ldN7J6yDgUzbX8Uk3sD4a4eZVPTBvqNDoUqcYMlZ9uuDdCvNv4TM3c1WzsXUrExwVkI1XO5jCNbgDJ25nkT/c1gIFvqoogl7MdSFGc4W4xZsqCItnqbypR3RuGIlR9h9vlRsy7zJR9PBI83X8alLDIfR1ukt1P7tMnmogZ0cuDY8cZsd8ZlCgLadmvej58SLsIkVxFJ8XyUgx9FmutKSYTmYtWBZ0+tNvfGmbXU7cob8H/4nLBiCwIUFluw==",timestamp="1554208460",serial_no="1DDE55AD98ED71D6EDD4A4A16996DE7B47773A8C"`}
})

最后请求示例

typescript
let timestamp = util.getTimestamp() // 用到的地方都是这个值
let nonceStr = util.getGuid() // 用到的地方都是这个值
let body = {
  appid: config.APPID,
  mchid: config.MCHID,
  description:'测试一下',
  out_trade_no: util.getGuid(),
  notify_url: "http: //simbajs.com:8089/notify',
  amount: { total: money },
    payer: { openid }
}
let AuthSecondParams = {
  mchid,
  serial_no,
  nonce_str,
  timestamp,
  signature:'xxxxxxxxxxxxxxxxxx' // 计算后的签名值
}
function toJoin(obj){
  return Object.keys(obj).map(k=>`${k}="${obj[k]}"`).join()
}
const PAY_API = 'https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi'
const Authorization = 'WECHATPAY2-SHA256-RSA2048'+' '+ toJoin(AuthSecondParams)
let { data } = await axios.post(PAY_API, body,{
  headers:{ 
    'Authorization':Authorization
  }
})

后端返回预支付订单号

前面一切顺利就可以拿到prepay_id了 后台需要给前端返回前端需要的数据 前端需要的数据也就是小程序调支付( wx.requestPayment)要的数据 看这里 https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_4.shtmlimage.png 上面paySign使用signature是不对的,还得重新计算一遍另一个签名值 查看文档可知 image.png 计算paySign签名值

typescript
签名串一共有四行,每一行为一个参数。行尾以\n(换行符,ASCII编码值为0x0A)结束,
包括最后一行。
如果参数本身以\n结束,也需要附加一个\n

// 格式
小程序appId\n
时间戳\n
随机字符串\n
订单详情扩展字符串\n
// 实例
wx8888888888888888\n
1414561699\n
5K8264ILTKCH16CQ2502SI8ZNMTM67VS\n
prepay_id=wx201410272009395522657a690389285100\n

// 老样子还是用之前的计算签名值方法  sha256 with rsa base64那套

const Rsa = require('./rsa.js); // 就是第一个方案的文件
function calcSign(msg){
  let pem = fs.readFileSync('./apiclient_key.pem')
  return Rsa.sign(msg,pem).toString('base64')
}
function joinMessage(...args){
  return args.join('\n)+'\n'
                   }
let package ="prepay_id= + prepay_id
let paySign = calcSign(joinMessage(config.APPID,timeStamp,nonceStr,package))

let { data:{prepay_id} } = await axios.post( 'https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi ', body,{
  headers:{ 'Authorization':Authorization});
  
  res.json({
  nonceStr,
  package: package,
  paysign: paysign,
  timeStamp,
  signType:"RSA'
})

前端接收

小程序需要的参数看这里 https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_4.shtml

参数名变量类型[长度限制]必填描述
小程序IDappIdstring[1,32]商户申请的小程序对应的appid,由微信支付生成,可在小程序后台查看
示例值:wx8888888888888888
时间戳timeStampstring[1,32]时间戳,标准北京时间,时区为东八区,自1970年1月1日 0点0分0秒以来的秒数。注意:部分系统取到的值为毫秒级,需要转换成秒(10位数字)。
示例值:1414561699
随机字符串nonceStrstring[1,32]随机字符串,不长于32位。推荐随机数生成算法
示例值:5K8264ILTKCH16CQ2502SI8ZNMTM67VS
订单详情扩展字符串packagestring[1,128]小程序下单接口返回的prepay_id参数值,提交格式如:prepay_id=***
示例值:prepay_id=wx201410272009395522657a690389285100
签名方式signTypestring[1,32]签名类型,默认为RSA,仅支持RSA。
示例值:RSA
签名paySignstring[1,512]签名,使用字段appId、timeStamp、nonceStr、package计算得出的签名值
示例值:oR9d8PuhnIc+YZ8cB==

扫码支付即可

end

粤ICP备2024285819号