v3 微信api 请求微信_一文搞懂微信支付 Api-v3 规则实现(附源码)

v2 与 v3 的区别

先看看 v2 与 v3 的区别,做到心中有数不怯场:)

规则差异

JSON

参数格式

XML

POST、GET 或 DELETE

提交方式

POST

AES-256-GCM加密

回调加密

无需加密

RSA 加密

敏感加密

无需加密

UTF-8

编码方式

UTF-8

非对称密钥SHA256-RSA

签名方式

MD5 或 HMAC-SHA256

微信支付Api-v3 规则 官方文档 ,此规则需要你耐心细品,重复多此细品效果更佳。

以下是我细品后,总结的实现方案,在这里就分享给大家,干货多屁话少直接贴实现。

Talk is cheap. Show me the code.

获取证书序列号

通过代码获取

这里我们使用第三方的库 x509,如你知道其它获取方法欢迎留言

const cert = x509.parseCert('cert.pem 证书绝对路径')

console.log(`证书序列号:${cert.serial}`)

通过工具获取

openssl x509 -in apiclient_cert.pem -noout -serial

构建请求头

1、构建请求签名参数

/**

* 构建请求签名参数

* @param method {RequestMethod} Http 请求方式

* @param url 请求接口 /v3/certificates

* @param timestamp 获取发起请求时的系统当前时间戳

* @param nonceStr 随机字符串

* @param body 请求报文主体

*/

public static buildReqSignMessage(method: RequestMethod, url: string, timestamp: string, nonceStr: string, body: string): string {

return method

.concat('\n')

.concat(url)

.concat('\n')

.concat(timestamp)

.concat('\n')

.concat(nonceStr)

.concat('\n')

.concat(body)

.concat('\n')

}

2、使用 SHA256 with RSA 算法生成签名

/**

* SHA256withRSA

* @param data 待加密字符

* @param privatekey 私钥key key.pem fs.readFileSync(keyPath)

*/

public static sha256WithRsa(data: string, privatekey: Buffer): string {

return crypto

.createSign('RSA-SHA256')

.update(data)

.sign(privatekey, 'base64')

}

3、根据平台规则生成请求头 authorization

/**

* 获取授权认证信息

*

* @param mchId 商户号

* @param serialNo 商户API证书序列号

* @param nonceStr 请求随机串

* @param timestamp 时间戳

* @param signature 签名值

* @param authType 认证类型,目前为WECHATPAY2-SHA256-RSA2048

*/

public static getAuthorization(mchId: string, serialNo: string, nonceStr: string, timestamp: string, signature: string, authType: string): string {

let map: Map = new Map()

map.set('mchid', mchId)

map.set('serial_no', serialNo)

map.set('nonce_str', nonceStr)

map.set('timestamp', timestamp)

map.set('signature', signature)

return authType.concat(' ').concat(this.createLinkString(map, ',', false, true))

}

4、Show Time

/**

* 构建 v3 接口所需的 Authorization

*

* @param method {RequestMethod} 请求方法

* @param urlSuffix 可通过 WxApiType 来获取,URL挂载参数需要自行拼接

* @param mchId 商户Id

* @param serialNo 商户 API 证书序列号

* @param key key.pem 证书

* @param body 接口请求参数

*/

public static async buildAuthorization(method: RequestMethod, urlSuffix: string, mchId: string, serialNo: string, key: Buffer, body: string): Promise {

let timestamp: string = parseInt((Date.now() / 1000).toString()).toString()

let authType: string = 'WECHATPAY2-SHA256-RSA2048'

let nonceStr: string = Kits.generateStr()

// 构建签名参数

let buildSignMessage: string = this.buildReqSignMessage(method, urlSuffix, timestamp, nonceStr, body)

// 生成签名

let signature: string = this.sha256WithRsa(key, buildSignMessage)

// 根据平台规则生成请求头 authorization

return this.getAuthorization(mchId, serialNo, nonceStr, timestamp, signature, authType)

}

封装网络请求

每个人都有个性,可使用的网络库也比较多(Axios、Fetch、Request 等),为了适配能适配这里做一代理封装。具体实现如下,网络请求库默认是使用的 Axios

1、抽离抽象接口

/**

* @author Javen

* @copyright javendev@126.com

* @description 封装网络请求工具

*/

export class HttpKit {

private static delegate: HttpDelegate = new AxiosHttpKit()

public static get getHttpDelegate(): HttpDelegate {

return this.delegate

}

public static set setHttpDelegate(delegate: HttpDelegate) {

this.delegate = delegate

}

}

export interface HttpDelegate {

httpGet(url: string, options?: any): Promise

httpGetToResponse(url: string, options?: any): Promise

httpPost(url: string, data: string, options?: any): Promise

httpPostToResponse(url: string, data: string, options?: any): Promise

httpDeleteToResponse(url: string, options?: any): Promise

httpPostWithCert(url: string, data: string, certFileContent: Buffer, passphrase: string): Promise

upload(url: string, filePath: string, params?: string): Promise

}

2、Axios 具体实现

/**

* @author Javen

* @copyright javendev@126.com

* @description 使用 Axios 实现网络请求

*/

import axios from 'axios'

import * as fs from 'fs'

import { HttpDelegate } from './HttpKit'

import * as FormData from 'form-data'

import * as https from 'https'

import concat = require('concat-stream')

export class AxiosHttpKit implements HttpDelegate {

httpGet(url: string, options?: any): Promise {

return new Promise((resolve, reject) => {

axios

.get(url, options)

.then(response => {

if (response.status === 200) {

resolve(response.data)

} else {

reject(`error code ${response.status}`)

}

})

.catch(error => {

reject(error)

})

})

}

httpGetToResponse(url: string, options?: any): Promise {

return new Promise(resolve => {

axios

.get(url, options)

.then(response => {

resolve(response)

})

.catch(error => {

resolve(error.response)

})

})

}

httpPost(url: string, data: string, options?: any): Promise {

return new Promise((resolve, reject) => {

axios

.post(url, data, options)

.then(response => {

if (response.status === 200) {

resolve(response.data)

} else {

reject(`error code ${response.status}`)

}

})

.catch(error => {

reject(error)

})

})

}

httpPostToResponse(url: string, data: string, options?: any): Promise {

return new Promise(resolve => {

axios

.post(url, data, options)

.then(response => {

resolve(response)

})

.catch(error => {

resolve(error.response)

})

})

}

httpDeleteToResponse(url: string, options?: any): Promise {

return new Promise(resolve => {

axios

.delete(url, options)

.then(response => {

resolve(response)

})

.catch(error => {

resolve(error.response)

})

})

}

httpPostWithCert(url: string, data: string, certFileContent: Buffer, passphrase: string): Promise {

return new Promise((resolve, reject) => {

let httpsAgent = new https.Agent({

pfx: certFileContent,

passphrase

})

axios

.post(url, data, { httpsAgent })

.then(response => {

if (response.status === 200) {

resolve(response.data)

} else {

reject(`error code ${response.status}`)

}

})

.catch(error => {

reject(error)

})

})

}

upload(url: string, filePath: string, params?: string): Promise {

return new Promise((resolve, reject) => {

let formData = new FormData()

formData.append('media', fs.createReadStream(filePath))

if (params) {

formData.append('description', params)

}

formData.pipe(

concat({ encoding: 'buffer' }, async data => {

axios

.post(url, data, {

headers: {

'Content-Type': 'multipart/form-data'

}

})

.then(response => {

if (response.status === 200) {

resolve(response.data)

} else {

reject(`error code ${response.status}`)

}

})

.catch(error => {

reject(error)

})

})

)

})

}

}

3、使其支持 Api-v3 接口规则

/**

* 微信支付 Api-v3 get 请求

* @param urlPrefix

* @param urlSuffix

* @param mchId

* @param serialNo

* @param key

* @param params

*/

public static async exeGet(urlPrefix: string, urlSuffix: string, mchId: string, serialNo: string, key: Buffer, params?: Map): Promise {

if (params && params.size > 0) {

urlSuffix = urlSuffix.concat('?').concat(this.createLinkString(params, '&', true, false))

}

let authorization = await this.buildAuthorization(RequestMethod.GET, urlSuffix, mchId, serialNo, key, '')

return await this.get(urlPrefix.concat(urlSuffix), authorization, serialNo)

}

/**

* 微信支付 Api-v3 post 请求

* @param urlPrefix

* @param urlSuffix

* @param mchId

* @param serialNo

* @param key

* @param data

*/

public static async exePost(urlPrefix: string, urlSuffix: string, mchId: string, serialNo: string, key: Buffer, data: string): Promise {

let authorization = await this.buildAuthorization(RequestMethod.POST, urlSuffix, mchId, serialNo, key, data)

return await this.post(urlPrefix.concat(urlSuffix), data, authorization, serialNo)

}

/**

* 微信支付 Api-v3 delete 请求

* @param urlPrefix

* @param urlSuffix

* @param mchId

* @param serialNo

* @param key

*/

public static async exeDelete(urlPrefix: string, urlSuffix: string, mchId: string, serialNo: string, key: Buffer): Promise {

let authorization = await this.buildAuthorization(RequestMethod.DELETE, urlSuffix, mchId, serialNo, key, '')

return await this.delete(urlPrefix.concat(urlSuffix), authorization, serialNo)

}

/**

* get 方法

* @param url 请求 url

* @param authorization 授权信息

* @param serialNumber 证书序列号

*/

public static async get(url: string, authorization: string, serialNumber?: string) {

return await HttpKit.getHttpDelegate.httpGetToResponse(url, {

headers: this.getHeaders(authorization, serialNumber)

})

}

/**

* post 方法

* @param url 请求 url

* @param authorization 授权信息

* @param serialNumber 证书序列号

*/

public static async post(url: string, data: string, authorization: string, serialNumber?: string) {

return await HttpKit.getHttpDelegate.httpPostToResponse(url, data, {

headers: this.getHeaders(authorization, serialNumber)

})

}

/**

* delete 方法

* @param url 请求 url

* @param authorization 授权信息

* @param serialNumber 证书序列号

*/

public static async delete(url: string, authorization: string, serialNumber?: string) {

return await HttpKit.getHttpDelegate.httpDeleteToResponse(url, {

headers: this.getHeaders(authorization, serialNumber)

})

}

/**

* 获取请求头

* @param authorization 授权信息

* @param serialNumber 证书序列号

*/

private static getHeaders(authorization: string, serialNumber: string): Object {

let userAgent: string = 'WeChatPay-TNWX-HttpClient/%s (%s) nodejs/%s'

userAgent = util.format(

userAgent,

'2.4.0',

os

.platform()

.concat('/')

.concat(os.release()),

process.version

)

return {

Authorization: authorization,

Accept: 'application/json',

'Content-type': 'application/json',

'Wechatpay-Serial': serialNumber,

'User-Agent': userAgent

}

}

如何使用?

这里以「获取平台证书」为例,来演示上面封装的系列方法如何使用

try {

let result = await PayKit.exeGet(

WX_DOMAIN.CHINA, //

WX_API_TYPE.GET_CERTIFICATES,

config.mchId,

x509.parseCert(config.certPath).serial,

fs.readFileSync(config.keyPath)

)

console.log(`result.data:${result.data}`)

// 应答报文主体

let data = JSON.stringify(result.data)

// 应答状态码

console.log(`status:${result.status}`)

console.log(`data:${data}`)

// http 请求头

let headers = result.headers

// 证书序列号

let serial = headers['wechatpay-serial']

// 应答时间戳

let timestamp = headers['wechatpay-timestamp']

// 应答随机串

let nonce = headers['wechatpay-nonce']

// 应答签名

let signature = headers['wechatpay-signature']

console.log(`serial:\n${serial}`)

console.log(`timestamp:\n${timestamp}`)

console.log(`nonce:\n${nonce}`)

console.log(`signature:\n${signature}`)

ctx.body = data

} catch (error) {

console.log(error)

}

至此微信支付 Api-v3 规则的接口已经测试通过。

但还有其他细节如要我们继续完善,比如 验证签名、证书和回调报文解密

证书和回调报文解密

AEAD_AES_256_GCM 解密算法实现

/**

* AEAD_AES_256_GCM 解密

* @param key apiKey3

* @param nonce 加密使用的随机串初始化向量

* @param associatedData 附加数据包

* @param ciphertext 密文

*/

public static aes256gcmDecrypt(key: string, nonce: string, associatedData: string, ciphertext: string): string {

let ciphertextBuffer = Buffer.from(ciphertext, 'base64')

let authTag = ciphertextBuffer.slice(ciphertextBuffer.length - 16)

let data = ciphertextBuffer.slice(0, ciphertextBuffer.length - 16)

let decipherIv = crypto.createDecipheriv('aes-256-gcm', key, nonce)

decipherIv.setAuthTag(Buffer.from(authTag))

decipherIv.setAAD(Buffer.from(associatedData))

let decryptStr = decipherIv.update(data, null, 'utf8')

decipherIv.final()

return decryptStr

}

保存微信平台证书示例

// 证书和回调报文解密

let certPath = '/Users/Javen/cert/platform_cert.pem'

try {

let decrypt = PayKit.aes256gcmDecrypt(

config.apiKey3,

ctx.app.config.AEAD_AES_256_GCM.nonce,

ctx.app.config.AEAD_AES_256_GCM.associated_data,

ctx.app.config.AEAD_AES_256_GCM.ciphertext

)

// 保存证书

fs.writeFileSync(certPath, decrypt)

ctx.body = decrypt

} catch (error) {

console.log(error)

}

验证签名

示例

// 根据序列号查证书 验证签名

let verifySignature: boolean = PayKit.verifySignature(signature, data, nonce, timestamp, fs.readFileSync(ctx.app.config.WxPayConfig.wxCertPath))

console.log(`verifySignature:${verifySignature}`)

构建应答签名参数

/**

* 构建应答签名参数

* @param timestamp 应答时间戳

* @param nonceStr 应答随机串

* @param body 应答报文主体

*/

public static buildRepSignMessage(timestamp: string, nonceStr: string, body: string): string {

return timestamp

.concat('\n')

.concat(nonceStr)

.concat('\n')

.concat(body)

.concat('\n')

}

使用平台证书验证

/**

* 验证签名

* @param signature 待验证的签名

* @param body 应答主体

* @param nonce 随机串

* @param timestamp 时间戳

* @param publicKey 平台公钥

*/

public static verifySignature(signature: string, body: string, nonce: string, timestamp: string, publicKey: Buffer): boolean {

// 构建响应体中待签名数据

let buildSignMessage: string = this.buildRepSignMessage(timestamp, nonce, body)

return Kits.sha256WithRsaVerify(publicKey, signature, buildSignMessage)

}

/**

* SHA256withRSA 验证签名

* @param publicKey 公钥key

* @param signature 待验证的签名串

* @param data 需要验证的字符串

*/

public static sha256WithRsaVerify(publicKey: Buffer, signature: string, data: string) {

return crypto

.createVerify('RSA-SHA256')

.update(data)

.verify(publicKey, signature, 'base64')

}

TNWX: TypeScript + Node.js + WeiXin 微信系开发脚手架,支持微信公众号、微信支付、微信小游戏、微信小程序、企业微信/企业号、企业微信开放平台。最最最重要的是能快速的集成至任何 Node.js 框架(Express、Nest、Egg、Koa 等)

微信支付已支持 Api-v3 以及 Api-v2 版本接口,同时支持多商户多应用,国内与境外的普通商户模式和服务商模式,v2 接口同时支持 MD5 以及 HMAC-SHA256 签名算法。

如有疑问欢迎留言或者站内私信。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值