由于项目需要,对接了一下最新的apiv3版的微信支付,花了不少时间,为避免大家走弯路,把详细的对接过程给大家分析一下。
首先上官网开发者文档copy一下代码!由于我们这次接通的是小程序的支付,所以基本代码都在后端。
https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_8_2.shtml 小程序支付开发指引
整体的流程大致是 统一下单 — 支付通知
先上一段代码
/*
Package core 微信支付api v3 go http-client 基础库,你可以使用它来创建一个client,并向微信支付发送http请求
只需要你在初始化客户端的时候,传递credential以及validator
credential用来生成http header中的authorization信息
validator则用来校验回包是否被篡改
如果http请求返回的err为nil,一般response.Body 都不为空,你可以尝试对其进行序列化
请注意及时关闭response.Body
注意:使用微信支付apiv3 go库需要引入相关的包,该示例代码必须引入的包名有以下信息
"context"
"crypto/x509"
"fmt"
"io/ioutil"
"log"
"github.com/wechatpay-apiv3/wechatpay-go/core"
"github.com/wechatpay-apiv3/wechatpay-go/core/option"
"github.com/wechatpay-apiv3/wechatpay-go/utils"
*/
func SetUp() (opt []option.ClientOption, err error) {
//商户号
mchID := ""
//商户证书序列号
mchCertSerialNumber := ""
//商户私钥文件路径
privateKeyPath := ""
//平台证书文件路径
wechatCertificatePath := ""
// 加载商户私钥
privateKey, err := utils.LoadPrivateKeyWithPath(privateKeyPath)
if err != nil {
log.Printf("load private err:%s", err.Error())
return nil, err
}
// 加载微信支付平台证书
wechatPayCertificate, err := utils.LoadCertificateWithPath(wechatCertificatePath)
if err != nil {
log.Printf("load certificate err:%s",err)
return nil, err
}
//设置header头中authorization信息
opts := []option.ClientOption{
option.WithMerchant(mchID, mchCertSerialNumber, privateKey), // 设置商户相关配置
option.WithWechatPay([]*x509.Certificate{wechatPayCertificate}), // 设置微信支付平台证书,用于校验回包信息用
}
return opts, nil
}
这段代码是为了统一下单发出请求做的前置准备,配置了一些必要的加密和签名参数,在发起请求前会需要签名和加密的数据包装好,具体的先不开,只解析其中需要配置的参数。
mchCertSerialNumber是商户证书序列号,登录微信商户平台去申请好证书,具体位置是 交易中心–api安全 然后右侧会有三个模块 从上到下 第一个就是申请证书,第二个是老版的,第三个是apiv3密码。证书申请的时候必须用window系统的,申请完毕之后就会有查看证书,点击查看证书,就会有商户证书序列号了。
privateKeyPath 商户私钥文件路径,wechatCertificatePath 平台证书文件路径
如图apiclient_key.pem是privateKeyPath,apiclient_cert.pem是wechatCertificatePath,另一个p12是windows平台上用的,我没接,就不细说了。
上一段代码 setup函数中 有这样一段代码:
//设置header头中authorization信息
opts := []option.ClientOption{
option.WithMerchant(mchID, mchCertSerialNumber, privateKey), // 设置商户相关配置
option.WithWechatPay([]*x509.Certificate{wechatPayCertificate}), // 设置微信支付平台证书,用于校验回包信息用
}
先做标记,待会说!
接着要发起请求了 上代码
func CreateOrder() {
// 初始化客户端
ctx := context.TODO()
opts, err := SetUp() //第一段代码中的函数
if err != nil {
return
}
client, err := core.NewClient(ctx, opts...,)
if err != nil{
log.Printf("init client err:%s",err)
return
}
//设置请求地址
URL := "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi"
//设置请求信息,此处也可以使用结构体来进行请求
mapInfo := map[string]interface{}{
"mchid": "1900006XXX",
"out_trade_no": "1217752501201407033233368318",
"appid": "wxdace645e0bc2cXXX",
"description": "Image形象店-深圳腾大-QQ公仔",
"notify_url": "https://weixin.qq.com/",
"amount": map[string]interface{}{
"total": 1,
"currency": "CNY",
},
"payer": map[string]interface{}{
"openid": "o4GgauInH_RCEdvrrNGrntXDuXXX",
},
}
// 发起请求
response, err := client.Post(ctx, URL, mapInfo)
if err != nil{
log.Printf("client post err:%s",err)
return
}
// 校验回包内容是否有逻辑错误
err = core.CheckResponse(response)
if err != nil{
log.Printf("check response err:%s",err)
return
}
// 读取回包信息
body, err := ioutil.ReadAll(response.Body)
if err != nil{
log.Printf("read response body err:%s",err)
return
}
fmt.Println(string(body))
}
这是原版的代码,其中content.TODO()就是标准包里的包,可以按着第一段代码中注释的引入。代码基本无问题按着自己的逻辑修改即可。但是!!!!!会有报错
client post err:validate verify fail serial=4F601336BA663C5C17CE5A6CA05E80DC9E2DE5A5 request-id=08FCDF8D840610D80518EDAAB74C20F60328C867-0 err=no serial number:4F601336BA663C5C17CE5A6CA05E80DC9E2DE5A5 corresponding certificate
这个问题在微信给的官方包github中有解答,解决方案是:
setup函数中 我们单独拉出来了一段代码修改一下
//设置header头中authorization信息
opts := []option.ClientOption{
option.WithMerchant(mchID, mchCertSerialNumber, privateKey), // 设置商户相关配置
option.WithWechatPay([]*x509.Certificate{wechatPayCertificate}), // 设置微信支付平台证书,用于校验回包信息用
option.WithoutValidator(),//跳过证书的效验
}
多出来一行代码option.WithoutValidator(),//跳过证书的效验
就可以拿出来prepay_id了
接着组装数据,进行签名
我们先看官方的文档https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_4.shtml 其中详细讲解了签名方式,但是没有给签名函数,文档中有个关键的话 (使用商户私钥对待签名串进行SHA256 with RSA签名)且同发起支付时的签名方式一致!!!!!!这就好说,在他的前置函数setup中一定有相关的函数可以用,我们来找找
option.WithMerchant(mchID, mchCertSerialNumber, privateKey),
这段中使用了私钥,肯定有加密 进去看看
// WithMerchant 通过商户号、商户证书序列号、私钥构建一个默认的credential的ClientOption,用于生成http request header 中authorization信息
func WithMerchant(mchID, certificateSerialNo string, privateKey *rsa.PrivateKey) ClientOption {
credential := &credentials.WechatPayCredentials{
Signer: &signers.Sha256WithRSASigner{PrivateKey: privateKey, MchCertificateSerialNo: certificateSerialNo},
MchID: mchID,
}
return withCredential{credential: credential}
}
进去Sha256WithRSASigner中看看去
package signers
import (
"context"
"crypto/rsa"
"fmt"
"strings"
"github.com/wechatpay-apiv3/wechatpay-go/core/auth"
)
// Sha256WithRSASigner Sha256WithRSA 签名器
type Sha256WithRSASigner struct {
MchCertificateSerialNo string // 商户证书序列号
PrivateKey *rsa.PrivateKey // 商户私钥
}
// GetName 获取签名器的名称
func (s *Sha256WithRSASigner) GetName() string {
return "SHA256withRSA"
}
// 获取签名器的类型
func (s *Sha256WithRSASigner) GetType() string {
return "PRIVATEKEY"
}
// 获取签名器的版本
func (s *Sha256WithRSASigner) GetVersion() string {
return "1.0"
}
// 对信息使用Sha256WithRsa的方式进行签名
func (s *Sha256WithRSASigner) Sign(ctx context.Context, message string) (*auth.SignatureResult, error) {
if s.PrivateKey == nil {
return nil, fmt.Errorf("you must set privatekey to use Sha256WithRSASigner")
}
if strings.TrimSpace(s.MchCertificateSerialNo) == "" {
return nil, fmt.Errorf("you must set mch certificate serial no to use Sha256WithRSASigner")
}
signature, err := Sha256WithRsa(message, s.PrivateKey)
if err != nil {
return nil, err
}
return &auth.SignatureResult{MchCertificateSerialNo: s.MchCertificateSerialNo, Signature: signature}, nil
}
好了 这么快就找到签名函数了 signature, err := Sha256WithRsa(message, s.PrivateKey) 就是我们要找的签名函数,和官方说的一样
给大家看一下我的签名方式
//开始签名
timeStamp:=strconv.FormatInt(time.Now().Unix(),10)
nonceStr:=utils.Md5(utils.RandChar(6))
packageStr:="prepay_id="+prepaymap["prepay_id"]
signBody:=appid+"\n"
signBody+=timeStamp+"\n"
signBody+=nonceStr+"\n"
signBody+=packageStr+"\n"
privateKeyPath := "./cert/apiclient_key.pem"
privateKey, err := wx.LoadPrivateKeyWithPath(privateKeyPath)//我本身有utils包 所以吧微信的utils包重命名为了wx
/*
我引入包的方式
"github.com/wechatpay-apiv3/wechatpay-go/core"
"github.com/wechatpay-apiv3/wechatpay-go/core/auth/signers"
"github.com/wechatpay-apiv3/wechatpay-go/core/option"
wx "github.com/wechatpay-apiv3/wechatpay-go/utils"
*/
//Signer:=&signers.Sha256WithRSASigner{PrivateKey: privateKey, MchCertificateSerialNo: mchCertSerialNumber}
sign,err:=signers.Sha256WithRsa(signBody,privateKey)
resultMap:=make(map[string]string)
resultMap["timeStamp"]=timeStamp
resultMap["nonceStr"]=nonceStr
resultMap["package"]=packageStr
resultMap["signType"]="RSA"
resultMap["paySign"]=sign
至此,统一下单就结束了小程序调起支付的参数我们也包装好传给前端了。注意包引入的方式!!!!!!!
接着做支付回调
我用的beego框架
直接上代码
//***微信支付回调
type WxPayNotify struct {
controllers.IndexController
}
func (this *WxPayNotify)Post(){
body:=this.Ctx.Input.RequestBody
apiv3Key:=beego.AppConfig.String("apiv3key")
var bodyMap map[string]interface{}
err:=json.Unmarshal(body, &bodyMap)
if err!=nil {
this.Data["json"]=map[string]interface{}{"code": "faild", "message": "数据解析失败"}
this.ServeJSON()
return
}
var resource map[string]interface{}
resource=make(map[string]interface{})
resource,ok:=bodyMap["resource"].(map[string]interface{})
if !ok{
this.Data["json"]=map[string]interface{}{"code": "false", "message": "bodyMap 解析错误"}
this.ServeJSON()
return
//return
}
jsonText,err:=wx.DecryptToString(apiv3Key,resource["associated_data"].(string),resource["nonce"].(string),resource["ciphertext"].(string))
var resultMap map[string]interface{}
err=json.Unmarshal([]byte(jsonText), &resultMap)
if err!=nil{
this.Data["json"]=map[string]interface{}{"code": "false", "message": "resultMap 解析错误"}
this.ServeJSON()
return
}
fmt.Println("resultMap:",resultMap)
order_id:=resultMap["out_trade_no"].(string)
money:=int64(resultMap["amount"].(map[string]interface{})["total"].(float64))
orderinfo,err:=order.GetOrderByOrderId(order_id)
if err!=nil{
this.Data["json"]=map[string]interface{}{"code": "false", "message": "订单不存在"}
this.ServeJSON()
return
}
err=order.OrderPaid(order_id,money)
if err!=nil{
this.Data["json"]=map[string]interface{}{"code": "false", "message": err.Error()}
this.ServeJSON()
return
}
service_id,_:=service.GetService(orderinfo.StoreId)
change:=cmd.SendOrderStatus{Cmd:5,Uid:orderinfo.Uid,Storeid:orderinfo.StoreId,Status:orderinfo.Status}
go socket.SendMessageToPeer(service_id,change)
this.Data["json"]=map[string]interface{}{"code": "success", "message": "成功"}
this.ServeJSON()
return
}
文档说数据body是json格式,所以我们直接把body拿出来就是所需的json数据了
回调回来的数据还需要我们自己解密才行
官方文档中
参数解密 下面详细描述对通知数据进行解密的流程:
1、用商户平台上设置的APIv3密钥【微信商户平台—>账户设置—>API安全—>设置APIv3密钥】,记为key;
2、针对resource.algorithm中描述的算法(目前为AEAD_AES_256_GCM),取得对应的参数nonce和associated_data;
3、使用key、nonce和associated_data,对数据密文resource.ciphertext进行解密,得到JSON形式的资源对象;注:
AEAD_AES_256_GCM算法的接口细节,请参考rfc5116。微信支付使用的密钥key长度为32个字节,随机串nonce长度12个字节,associated_data长度小于16个字节并可能为空。
我们注意到算法AEAD_AES_256_GCM AES!!!!!!
在去翻翻微信的包 看看有没有这个方法
有个aes.go 看看是不是它!
package utils
import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
)
// DecryptToString 将下载证书的回包解析成证书
//
// 解析后的证书是这样的
// -----BEGIN CERTIFICATE-----
// -----END CERTIFICATE-----
func DecryptToString(apiv3Key, associatedData, nonce, ciphertext string) (certificate string, err error) {
decodedCiphertext, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return "", err
}
c, err := aes.NewCipher([]byte(apiv3Key))
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(c)
if err != nil {
return "", err
}
certificateByte, err := gcm.Open(nil, []byte(nonce), decodedCiphertext, []byte(associatedData))
if err != nil {
return "", err
}
certificate = string(certificateByte)
return certificate, nil
}
看这函数,它不就是告诉我们,直接传参数就可以了!!!!! 直接用吧!!!
至此,我们这次微信小程序的支付就对接完毕了,有什么不懂地方需要帮助的,可以来安阳APP开发找我!