golang 微信支付全解析

由于项目需要,对接了一下最新的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开发找我!

  • 4
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值