- 一文搞懂微信支付 Api-v3 规则实现(附源码)
v2 与 v3 的区别 先看看 v2 与 v3 的区别,做到心中有数不怯场:) V3 规则差异 V2 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 使用证书解析工具 https://myssl.com/cert_decode.html 构建请求头 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<string, string> = 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<string> { 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<any> httpGetToResponse(url: string, options?: any): Promise<any> httpPost(url: string, data: string, options?: any): Promise<any> httpPostToResponse(url: string, data: string, options?: any): Promise<any> httpDeleteToResponse(url: string, options?: any): Promise<any> httpPostWithCert(url: string, data: string, certFileContent: Buffer, passphrase: string): Promise<any> upload(url: string, filePath: string, params?: string): Promise<any> } [代码] 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<any> { 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<any> { 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<any> { 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<any> { return new Promise(resolve => { axios .post(url, data, options) .then(response => { resolve(response) }) .catch(error => { resolve(error.response) }) }) } httpDeleteToResponse(url: string, options?: any): Promise<any> { 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<any> { 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<any> { 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<string, string>): Promise<any> { 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<any> { 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<any> { 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') } [代码] 完整示例代码 Egg-TNWX TNWX: TypeScript + Node.js + WeiXin 微信系开发脚手架,支持微信公众号、微信支付、微信小游戏、微信小程序、企业微信/企业号、企业微信开放平台。最最最重要的是能快速的集成至任何 Node.js 框架(Express、Nest、Egg、Koa 等) 微信支付已支持 Api-v3 以及 Api-v2 版本接口,同时支持多商户多应用,国内与境外的普通商户模式和服务商模式,v2 接口同时支持 MD5 以及 HMAC-SHA256 签名算法。 如有疑问欢迎留言或者站内私信。
2020-04-21 - 微信支付v3 回调通知 验证签名出错 20211?
支付成功后回调通知,验签一直不能通过,用工具验证提示:验证签名出错!ret:20211(对签名串Base64解码失败) 验签证书使用的是微信平台的公共证书 收到的参数如下: [Wechatpay-Signature] => PYBEWw4yluNSdes7wMnMsneDlZOhIUB4SgCQ5mMITVAiAW2T1HF034HEuhBaKC9kJSnx55THswgnWMexzeoDBtPDAN5TTRxXAeVkzyGIjwJ4wP0xCSslx78OotVmMXYcftpVM4F8Ue3peacObYCWNIiCepitik2Z05+eZMWUEf\/gCtiwYhQ7V4QqAXSJbd+Qf2GkNHx2PHYyPFD129pDf+Xb5+9UUpO13YaNH7w\/Cv0FAHhjTKchcxuEJmpkUaoPtNqQ1k4E4R6BcX4\/wJa2EMy\/0tse4xeoKAaJNWwQK6UzziaQGSe6oSEI8XfOvGbLim1V+ZHbOEXIj2I6Z6PDTA== [Wechatpay-TimeStamp] => 1605690156 [Wechatpay-Nonce] => a2DkytR3PBXhuzlhBgcpVWFh0lmZahRq [Wechatpay-Serial] => 607D2488CF07CC41BC8A050D5B97A40D88E1CA35 body: {"id":"b3cd9e12-734b-5d2e-b07c-3d066814177f","create_time":"2020-11-18T17:02:36+08:00","resource_type":"encrypt-resource","event_type":"TRANSACTION.SUCCESS","summary":"支付成功","resource":{"original_type":"transaction","algorithm":"AEAD_AES_256_GCM","ciphertext":"1Ji7HsKaK4OspmKN2VsMQ0zSMsit1XEjljlW1W6PHQ5wT/PUzpkD1Nx/qeqtuY635RX4lcnptZqgmpbtdBcvMFyDzbiFNXyfeXl1ZhOz3bXdedZZKCvuJhVQ+S6eSYWJkRb/t4s4DRrcKYHa9UHtuGp/asLtVD6ThjcfquH5voGWrsD5RQke5KeyARIODSgUB4opEE/oOXU7VRi7OyqH9Coj8zbbS9D6BS5ymLiOitXZ+Ke1MTSXHvcRYP07n6X0fHUIM7oSi/T6pBdmwHv49BXeKjV90nW3nTR2cYwlxjhvQ9oa6XIdYJlsVNO+ICXTTn2KErFN6w7sK1p6uLe7alk3h1arcGFl2iasCPu7Zykibh6i5lBpBvvDprFtye99PkmND7w/rnCmC4y9msSsyx9T+vokVzel1EyGRQNcJtAW67vlRcz0b8IbcimLp+Q+O0aveUy0e69bGujoUfXqu6LnXkEX/AlG4sQZ5Ofghl3ggSiS4M7zS+AKI26JxrSM5CIFLCeHCN4z+rJFuXtZRdnw+uqFwVjTZZ0nhr4woVFFGYCQByPxl4eAjYq9V7x7yLY=","associated_data":"transaction","nonce":"xkPG9z5j8OQr"}} 验签名串: 1605690156 a2DkytR3PBXhuzlhBgcpVWFh0lmZahRq {"id":"b3cd9e12-734b-5d2e-b07c-3d066814177f","create_time":"2020-11-18T17:02:36+08:00","resource_type":"encrypt-resource","event_type":"TRANSACTION.SUCCESS","summary":"支付成功","resource":{"original_type":"transaction","algorithm":"AEAD_AES_256_GCM","ciphertext":"1Ji7HsKaK4OspmKN2VsMQ0zSMsit1XEjljlW1W6PHQ5wT\/PUzpkD1Nx\/qeqtuY635RX4lcnptZqgmpbtdBcvMFyDzbiFNXyfeXl1ZhOz3bXdedZZKCvuJhVQ+S6eSYWJkRb\/t4s4DRrcKYHa9UHtuGp\/asLtVD6ThjcfquH5voGWrsD5RQke5KeyARIODSgUB4opEE\/oOXU7VRi7OyqH9Coj8zbbS9D6BS5ymLiOitXZ+Ke1MTSXHvcRYP07n6X0fHUIM7oSi\/T6pBdmwHv49BXeKjV90nW3nTR2cYwlxjhvQ9oa6XIdYJlsVNO+ICXTTn2KErFN6w7sK1p6uLe7alk3h1arcGFl2iasCPu7Zykibh6i5lBpBvvDprFtye99PkmND7w\/rnCmC4y9msSsyx9T+vokVzel1EyGRQNcJtAW67vlRcz0b8IbcimLp+Q+O0aveUy0e69bGujoUfXqu6LnXkEX\/AlG4sQZ5Ofghl3ggSiS4M7zS+AKI26JxrSM5CIFLCeHCN4z+rJFuXtZRdnw+uqFwVjTZZ0nhr4woVFFGYCQByPxl4eAjYq9V7x7yLY=","associated_data":"transaction","nonce":"xkPG9z5j8OQr"}} [图片] [图片] [图片] 搞了一天,硬是没明白问题出在哪里。
2020-11-18 - .Net Core(C#)微信支付签名和验签最全代码共享
总所周知,C#早已经进入.NetCore跨平台时代了,我们已经在Linux上运行.NetCore好几年了。。。但微信支付的官方技术文档,在C#方面特别落后。很多代码并不兼容Linux,例如签名生成 https://wechatpay-api.gitbook.io/wechatpay-api-v3/qian-ming-zhi-nan-1/qian-ming-sheng-cheng 为了方便大伙,我特地把我的C#代码共享出来,亲测Cent OS完美运行。 1)首先,在C#里,无论公钥还是私钥,都是XML格式的,所以很多时候,需要将C#的公钥/私钥格式和JAVA的公钥/私钥格式互转,所以先来个RSAKeyConvert.cs /// <summary> /// RSA密钥转换器 /// </summary> public abstract class RSAKeyConvert { /// <summary> /// RSA私钥格式转换,java->.net /// </summary> /// <param name="privateKey">java生成的RSA私钥</param> /// <returns></returns> public static string RSAPrivateKeyJava2DotNet(string privateKey) { var privateKeyParam =(RsaPrivateCrtKeyParameters)PrivateKeyFactory.CreateKey(Convert.FromBase64String(privateKey)); return string.Format( "<RSAKeyValue><Modulus>{0}</Modulus><Exponent>{1}</Exponent><P>{2}</P><Q>{3}</Q><DP>{4}</DP><DQ>{5}</DQ><InverseQ>{6}</InverseQ><D>{7}</D></RSAKeyValue>", Convert.ToBase64String(privateKeyParam.Modulus.ToByteArrayUnsigned()), Convert.ToBase64String(privateKeyParam.PublicExponent.ToByteArrayUnsigned()), Convert.ToBase64String(privateKeyParam.P.ToByteArrayUnsigned()), Convert.ToBase64String(privateKeyParam.Q.ToByteArrayUnsigned()), Convert.ToBase64String(privateKeyParam.DP.ToByteArrayUnsigned()), Convert.ToBase64String(privateKeyParam.DQ.ToByteArrayUnsigned()), Convert.ToBase64String(privateKeyParam.QInv.ToByteArrayUnsigned()), Convert.ToBase64String(privateKeyParam.Exponent.ToByteArrayUnsigned())); } /// <summary> /// RSA私钥格式转换,.net->java /// </summary> /// <param name="privateKey">.net生成的私钥</param> /// <returns></returns> public static string RSAPrivateKeyDotNet2Java(string privateKey) { XmlDocument doc = new XmlDocument(); doc.LoadXml(privateKey); BigInteger m = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("Modulus")[0].InnerText)); BigInteger exp = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("Exponent")[0].InnerText)); BigInteger d = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("D")[0].InnerText)); BigInteger p = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("P")[0].InnerText)); BigInteger q = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("Q")[0].InnerText)); BigInteger dp = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("DP")[0].InnerText)); BigInteger dq = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("DQ")[0].InnerText)); BigInteger qinv = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("InverseQ")[0].InnerText)); RsaPrivateCrtKeyParameters privateKeyParam = new RsaPrivateCrtKeyParameters(m, exp, d, p, q, dp, dq, qinv); PrivateKeyInfo privateKeyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(privateKeyParam); byte[] serializedPrivateBytes = privateKeyInfo.ToAsn1Object().GetEncoded(); return Convert.ToBase64String(serializedPrivateBytes); } /// <summary> /// RSA公钥格式转换,java->.net /// </summary> /// <param name="publicKey">java生成的公钥</param> /// <returns></returns> public static string RSAPublicKeyJava2DotNet(string publicKey) { RsaKeyParameters publicKeyParam = (RsaKeyParameters)PublicKeyFactory.CreateKey(Convert.FromBase64String(publicKey)); return string.Format("<RSAKeyValue><Modulus>{0}</Modulus><Exponent>{1}</Exponent></RSAKeyValue>", Convert.ToBase64String(publicKeyParam.Modulus.ToByteArrayUnsigned()), Convert.ToBase64String(publicKeyParam.Exponent.ToByteArrayUnsigned())); } /// <summary> /// RSA公钥格式转换,.net->java /// </summary> /// <param name="publicKey">.net生成的公钥</param> /// <returns></returns> public static string RSAPublicKeyDotNet2Java(string publicKey) { XmlDocument doc = new XmlDocument(); doc.LoadXml(publicKey); BigInteger m = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("Modulus")[0].InnerText)); BigInteger p = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("Exponent")[0].InnerText)); RsaKeyParameters pub = new RsaKeyParameters(false, m, p); SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(pub); byte[] serializedPublicBytes = publicKeyInfo.ToAsn1Object().GetDerEncoded(); return Convert.ToBase64String(serializedPublicBytes); } } 2)根据官方提供的技术文档进行修改重写:HttpHandler.cs // 使用方法 // HttpClient client = new HttpClient(new HttpHandler("{商户号}", "{商户证书序列号}","{商户私钥(私钥不包括私钥文件起始的-----BEGIN PRIVATE KEY-----,亦不包括结尾的-----END PRIVATE KEY-----)}")); // ... // var response = client.GetAsync("https://api.mch.weixin.qq.com/v3/certificates"); public class HttpHandler : DelegatingHandler { private readonly string merchantId; private readonly string serialNo; private readonly string privateKey; /// <summary> /// HTTP句柄 /// </summary> /// <param name="merchantId">商户号</param> /// <param name="merchantSerialNo">商户证书序列号</param> /// <param name="privateKey">商户私钥(私钥不包括私钥文件起始的-----BEGIN PRIVATE KEY-----,亦不包括结尾的-----END PRIVATE KEY-----)</param> public HttpHandler(string merchantId, string merchantSerialNo,string privateKey) { InnerHandler = new HttpClientHandler(); this.merchantId = merchantId; this.serialNo = merchantSerialNo; this.privateKey = privateKey; } protected async override Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { var auth = await BuildAuthAsync(request); string value = $"WECHATPAY2-SHA256-RSA2048 {auth}"; request.Headers.Add("Authorization", value); request.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.143 Safari/537.36"); request.Headers.Add("Accept", "application/json"); return await base.SendAsync(request, cancellationToken); } protected async Task<string> BuildAuthAsync(HttpRequestMessage request) { string method = request.Method.ToString(); string body = ""; if (method == "POST" || method == "PUT" || method == "PATCH") { var content = request.Content; body = await content.ReadAsStringAsync(); } string uri = request.RequestUri.PathAndQuery; var timestamp = DateTimeOffset.Now.ToUnixTimeSeconds(); string nonce = Path.GetRandomFileName(); string message = $"{method}\n{uri}\n{timestamp}\n{nonce}\n{body}\n"; string signature = Sign(message); return $"mchid=\"{merchantId}\",nonce_str=\"{nonce}\",timestamp=\"{timestamp}\",serial_no=\"{serialNo}\",signature=\"{signature}\""; } //此方法不支持linux平台 //protected string Sign(string message) //{ // // NOTE: 私钥不包括私钥文件起始的-----BEGIN PRIVATE KEY----- // // 亦不包括结尾的-----END PRIVATE KEY----- // //string privateKey = "{你的私钥}"; // byte[] keyData = Convert.FromBase64String(privateKey); // using (CngKey cngKey = CngKey.Import(keyData, CngKeyBlobFormat.Pkcs8PrivateBlob)) // using (RSACng rsa = new RSACng(cngKey)) // { // byte[] data = System.Text.Encoding.UTF8.GetBytes(message); // return Convert.ToBase64String(rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1)); // } //} protected string Sign(string message) { // SHA256withRSA //根据需要加签时的哈希算法转化成对应的hash字符节 //byte[] bt = Encoding.GetEncoding("utf-8").GetBytes(str); byte[] bt =Encoding.UTF8.GetBytes(message); var sha256 = new SHA256CryptoServiceProvider(); byte[] rgbHash = sha256.ComputeHash(bt); RSACryptoServiceProvider key = new RSACryptoServiceProvider(); var _privateKey = RSAKeyConvert.RSAPrivateKeyJava2DotNet(privateKey); key.FromXmlString(_privateKey); RSAPKCS1SignatureFormatter formatter = new RSAPKCS1SignatureFormatter(key); formatter.SetHashAlgorithm("SHA256");//此处是你需要加签的hash算法,需要和上边你计算的hash值的算法一致,不然会报错。 byte[] inArray = formatter.CreateSignature(rgbHash); return Convert.ToBase64String(inArray); } } 到这里,签名部分已完成,主要修改了Sign(string message)函数,将JAVA的私钥格式转化为C#的私钥格式(XML),然后再进行签名~搞定! 3)写完签名之后,再写一个验签类:WechatPayCer.cs和HttpSignature.cs /// <summary> /// 微信平台证书 /// </summary> public class WechatPayCer { /// <summary> /// 证书序列号 /// </summary> public string SerialNo { get; private set; } /// <summary> /// 证书内容 /// </summary> public string Data { get;private set; } /// <summary> /// 签名器 /// </summary> private ISigner Signer { get; set; } /// <summary> /// 微信平台证书 /// </summary> /// <param name="serialNo"></param> /// <param name="data"></param> public WechatPayCer(string serialNo,string data) { this.SerialNo = serialNo; this.Data = data.Trim().Replace("-----BEGIN CERTIFICATE-----", "").Replace("-----END CERTIFICATE-----", "").Replace("\r", "").Replace("\n", ""); var bs = Convert.FromBase64String(this.Data); var x509 = new X509Certificate2(bs); var rsa = x509.PublicKey.Key; var publickey = rsa.ToXmlString(false); publickey=RSAKeyConvert.RSAPublicKeyDotNet2Java(publickey); Signer = SignerUtilities.GetSigner("SHA256WithRSA"); var publicKeyParam = (RsaKeyParameters)PublicKeyFactory.CreateKey(Convert.FromBase64String(publickey)); Signer.Init(false, publicKeyParam); } /// <summary> /// 验证签名是否匹配 /// </summary> /// <param name="message"></param> /// <param name="signature"></param> /// <returns></returns> public bool Verify(string message, string signature) { var signature_bs = Convert.FromBase64String(signature); var message_bs = Encoding.UTF8.GetBytes(message); Signer.BlockUpdate(message_bs, 0, message_bs.Length); var r = Signer.VerifySignature(signature_bs); return r; } } /// <summary> /// 微信支付HTTP签名 /// </summary> public abstract class HttpSignature { /// <summary> /// 验证签名 /// </summary> /// <param name="response">HTTP响应</param> /// <param name="get_cer">根据证书序列获取证书</param> /// <returns>签名验证成功返回(true,主体内容),否则返回(false,null)</returns> public static async Task<(bool result,string body)> VerificateAsync(HttpResponseMessage response,Func<string, WechatPayCer> get_cer) { var ctx = response.Content; string body; switch (response.StatusCode) { case System.Net.HttpStatusCode.OK: body = await ctx.ReadAsStringAsync(); break; case System.Net.HttpStatusCode.NoContent: body = null; break; case System.Net.HttpStatusCode.ServiceUnavailable: throw (new Exception($"微信接口{response.RequestMessage.RequestUri.ToString()}响应错误:503 - Service Unavailable,服务不可用,过载保护")); case System.Net.HttpStatusCode.BadGateway: throw (new Exception($"微信接口{response.RequestMessage.RequestUri.ToString()}响应错误:502 - Bad Gateway,服务下线,暂时不可用")); case System.Net.HttpStatusCode.InternalServerError: throw (new Exception($"微信接口{response.RequestMessage.RequestUri.ToString()}响应错误:500 - Server Error,系统错误")); case System.Net.HttpStatusCode.TooManyRequests: throw (new Exception($"微信接口{response.RequestMessage.RequestUri.ToString()}响应错误:429 - Too Many Requests,请求超过频率限制")); case System.Net.HttpStatusCode.NotFound: throw (new Exception($"微信接口{response.RequestMessage.RequestUri.ToString()}响应错误:404 - Not Found,请求的资源不存在")); case System.Net.HttpStatusCode.Forbidden: throw (new Exception($"微信接口{response.RequestMessage.RequestUri.ToString()}响应错误:403 - Forbidden,权限异常")); case System.Net.HttpStatusCode.Unauthorized: throw (new Exception($"微信接口{response.RequestMessage.RequestUri.ToString()}响应错误:401 - Unauthorized,签名验证失败")); case System.Net.HttpStatusCode.BadRequest: throw (new Exception($"微信接口{response.RequestMessage.RequestUri.ToString()}响应错误:400 - Bad Request,协议或者参数非法")); default: throw (new Exception($"微信接口{response.RequestMessage.RequestUri.ToString()}响应错误:未知响应状态{(int)response.StatusCode}-{response.StatusCode.ToString()}")); }; string timestamp; string nonce; string signature; string serial; WechatPayCer cer; try { timestamp = response.Headers.GetValues("Wechatpay-Timestamp").First(); } catch { return (false,null); } try { nonce = response.Headers.GetValues("Wechatpay-Nonce").First(); } catch { return (false, null); } try { serial = response.Headers.GetValues("Wechatpay-Serial").First(); } catch { return (false, null); } cer= get_cer(serial); if (cer == null) { throw (new Exception($"微信接口{response.RequestMessage.RequestUri.ToString()}发生错误:未找到相应证书(序列号{serial})")); } try { signature = response.Headers.GetValues("Wechatpay-Signature").First(); } catch { return (false, null); } //将响应内容进行签名 var s = $"{timestamp}\n{nonce}\n{body??""}\n"; var r = cer.Verify(s,signature); if (r) { return (true, body); } else { return (false,null); } } } 同样的可以看到,在WechatPayCer类里的也引用了RSAKeyConvert.RSAPublicKeyDotNet2Java方法,将C#的公钥(XML格式)转化为JAVA的公钥格式,然后再进行验签~完美搞定! 其实方法并不难,只是很多人不知道C#的公钥和私钥都是XML格式,而JAVA的格式不是。然后导出套代码,要么就是windows运行成功了,linux报错不支持;要么就是各种报错。 强烈建议将该文档列为官方文档,顶替原有demo~
2020-03-08 - 【C#】WechatPay-API-v3 使用平台证书加密内容与应答|通知验签(SHA256 with RSA)
/// <summary> /// 使用微信支付平台证书公钥加密验签 /// https://wechatpay-api.gitbook.io/wechatpay-api-v3/qian-ming-zhi-nan-1/qian-ming-yan-zheng /// </summary> public class WXPlatform { public string PublicKey { get; private set; } private byte[] _publicKeyBytes { get; set; } /// <summary> /// 构造方法 /// </summary> /// <param name="publickey">-----BEGIN CERTIFICATE----- 开头的string,转为bytes->不需要每次都去</param> public WXPlatform(string publickey) { this.PublicKey = publickey; this._publicKeyBytes = Encoding.UTF8.GetBytes(publickey); } /// <summary> /// 最终提交请求时,需对敏感信息加密,如身份证、银行卡号。 /// 加密算法是RSA,使用从接口下载到的公钥进行加密,非后台下载到的私钥。 /// </summary> /// <param name="text">要加密的明文</param> /// <param name="publicKey"> </param> /// <returns></returns> public string Encrypt(string text) { var x509 = new X509Certificate2(this._publicKeyBytes); using (var rsa = (RSACryptoServiceProvider)x509.PublicKey.Key) { var buff = rsa.Encrypt(Encoding.UTF8.GetBytes(text), true); return Convert.ToBase64String(buff); } } /// <summary> /// 验证签名 /// </summary> /// <param name="signedString">私钥加密串-Wechatpay-Signature</param> /// <param name="signSourceString">验签名串-应答时间戳\n应答随机串\n应答报文主体\n</param> /// <returns></returns> public bool VerifySign(string signedString, string signSourceString) { var x509 = new X509Certificate2(this._publicKeyBytes); using (var rsa = (RSACryptoServiceProvider)x509.PublicKey.Key) { using (var sha256 = new SHA256CryptoServiceProvider()) { var b = rsa.VerifyData(Encoding.UTF8.GetBytes(signSourceString), sha256, Convert.FromBase64String(signedString)); return b; } } } }
2020-02-29 - api v3 支付通知签名验证失败
支付成功后,收到微信的异步通知,但是签名验证一直失败,请问是什么原因? 1、签名验证的 证书是使用的微信平台的公共证书; 2、签名的数据已经规定格式构造验签名串 3、已经检查平台证书序列号,是一致的 签名验证方法如下: [图片]
2020-09-23