golang 实现微信小程序自动退款及 API 证书配置

更新日期: 2022-09-28 阅读次数: 2185 字数: 1646 分类: 微信小程序

之前用 golang 实现微信支付, 为了偷懒就没有加自动退款功能。 因为本以为是个试验性项目也没有人会去退款,再就是退款需要配置 API 证书,看起来很麻烦。

没想到,项目有真实客户需求了,于是不得不补上退款功能。 同时,由于涉及到一个微信小程序多个商户号的支付,及退款问题,需要每个商户配置一套证书。

微信官方的退款文档

https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_4

主要看里面哪些参数是必填的。

API 证书

关于 API 证书的介绍文档

https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=4_3

微信支付接口中,涉及资金回滚的接口会使用到API证书,包括退款、撤销接口。商家在申请微信支付成功后,收到的相应邮件后,可以按照指引下载API证书,也可以按照以下路径下载:微信商户平台(pay.weixin.qq.com)-->账户中心-->账户设置-->API安全

退款相关的数据结构及函数

我使用的 golang 微信支付的三方库:

https://github.com/wleven/wxpay

我把其中涉及到退款的代码,摘了出来,方便理解:

config := entity.PayConfig{
   // 传入支付初始化参数
   AppID         string        // 商户/服务商 AppId(公众号/小程序)
   MchID         string        // 商户/服务商 商户号
   SubAppID      string        // 子商户公众号ID
   SubMchID      string        // 子商户商户号
   PayNotify     string        // 支付结果回调地址
   RefundNotify  string        // 退款结果回调地址
   Secret        string        // 微信支付密钥
   APIClientPath APIClientPath // API证书内容,使用V3接口必传
   SerialNo      string        // 证书编号,使用V3接口必传
}

// APIClientPath 微信支付API证书
// 虽然最新版的接口已经将这个由 string 改成了 byte array,我还是觉得 string 方便管理
// 但是参考 https://github.com/wleven/wxpay/issues/10
// 给出的理由也很合理
// 对于 SaaS 系统来说,保存着多个商户的证书文件,通常文件不会保存到服务器上,而将其保存在 S3 或其它对象存储中。因此无法直接设置证书文件路径。
type APIClientPath struct {
	Cert string // 证书路径
	Key  string // 私钥证书路径,使用V3接口必传
	Root string // 根证书路径
}
type APIClientPath struct {
	Cert []byte // 证书内容
	Key  []byte // 私钥证书内容,使用V3接口必传
	Root []byte // 根证书内容
}

// 申请退款
if data, err := wxpay.V2.Refund(V2.Refund{/* 传入参数 */}); err == nil {
}

// Refund 退款参数
type Refund struct {
	TransactionID string `json:"transaction_id,omitempty"`  // 微信支付ID
	OutTradeNo    string `json:"out_trade_no,omitempty"`    // 商户订单ID
	OutRefundNo   string `json:"out_refund_no,omitempty"`   // 商户系统内部的退款单号
	TotalFee      int    `json:"total_fee,omitempty"`       // 订单总金额,单位为分
	RefundFee     int    `json:"refund_fee,omitempty"`      // 退款总金额,订单总金额
	RefundFeeType string `json:"refund_fee_type,omitempty"` // 退款货币类型,需与支付一致,或者不填。
	RefundDesc    string `json:"refund_desc,omitempty"`     // 若商户传入,会在下发给用户的退款消息中体现退款原因
	RefundAccount string `json:"refund_account,omitempty"`  // 退款资金来源 仅针对老资金流商户使用
}

升级 wleven/wxpay 版本到最新 v1.3.1

因为更改了证书配置的字段类型。担心以后要是更新,还得再兼容,不如一步到位。

> go get github.com/wleven/wxpay@latest
go: downloading github.com/wleven/wxpay v1.3.1
go: upgraded github.com/wleven/wxpay v1.2.9 => v1.3.1

证书,密钥的区别

从微信后台下载的证书压缩包里有三个文件:

  • apiclient_key.p12: 证书 pkcs12 格式。包含了私钥信息的证书文件,为p12(pfx)格式,由微信支付签发给您用来标识和界定您的身份。windows上可以直接双击导入系统,导入过程中会提示输入证书密码,证书密码默认为您的商户号(如:1900006031)
  • apiclient_cert.pem: 证书 pem 格式。从 apiclient_cert.p12 中导出证书部分的文件,为 pem 格式。由于部分开发语言及系统环境不能直接使用 p12 格式的证书,所以需要 pem 格式的证书。
  • apiclient_key.pem: 证书密钥 pem 格式。从 apiclient_cert.p12 中导出密钥部分的文件,为 pem 格式

这三个文件的区别是什么?

  • apiclient_cert.p12 是商户证书文件,除 PHP 外的开发均使用此证书文件。而实际上我用的这个三方 golang 库也不能直接使用 p12 格式的证书。需要使用导出的 pem 格式。
  • 商户如果使用 .NET 环境开发,请确认 Framework 版本大于2.0,必须在操作系统上双击安装证书 apiclient_cert.p12 后才能被正常调用

手动导出 pem 格式证书的命令。虽然暂时用不到,留个记录。

openssl pkcs12 -clcerts -nokeys -in apiclient_cert.p12 -out apiclient_cert.pem
openssl pkcs12 -nocerts -in apiclient_cert.p12 -out apiclient_key.pem

如果想了解证书加密的原理, 及这几个文件的作用,可以看下面这个公众号文章,讲的非常细致,易理解:

https://mp.weixin.qq.com/s/Zr_tIlhAjH7v8I-L5FC31g

rootca.pem 去哪里下载

Ubuntu Server 系统下:

ls /etc/ssl/certs/ | grep Root_CA

可以看到内置很多的证书。使用其中的 DigiCert_Global_Root_CA.pem 即可。

x509: certificate signed by unknown authority

调用微信退款接口时,报错:

x509: certificate signed by unknown authority

看上去是我随意指定的 rootca.pem 证书有问题。

参考: https://www.cnblogs.com/Alex80/p/8917033.html

大致意思是,2018 年 5 月,微信支付的 HTTPS 服务器将证书更换为了 DigiCert 签发的证书。 而这个证书大部分操作系统是自带的。2018 年 3月后, 不再提供 CA 证书文件(rootca.pem)下载。

于是我将证书替换为了 DigiCert_Global_Root_CA.pem,就可以成功退款了。

//root, _ := ioutil.ReadFile("/etc/ssl/certs/GlobalSign_Root_CA.pem")
root, _ := ioutil.ReadFile("/etc/ssl/certs/DigiCert_Global_Root_CA.pem")

证书安全防护

  • 证书文件不能放在 web 服务器虚拟目录,应放在有访问权限控制的目录中,防止被他人下载;
  • 建议将证书文件名改为复杂且不容易猜测的文件名。这点想得倒是很周到,因为我要配置一堆三方的商户支付,本来想新建一个证书目录,然后下面是一堆子目录,每个商户一个,看来还是要把目录名字起的含糊一点比较好。

带证书的网络请求

看了一下这个库里的实现,学习如何在 http 请求里配置证书。

// 网络请求
func (c WxPay) request(url string, body io.Reader, cert bool) (map[string]string, error) {
	var client http.Client

	if cert {
		if err := c.checkClient(); err != nil {
			return nil, err
		}
		// 微信提供的 API 证书,证书和证书密钥 .pem 格式
		// certs, _ := tls.LoadX509KeyPair(c.config.APIClientPath.Cert, c.config.APIClientPath.Key)
		// https://pkg.go.dev/crypto/tls
		// func X509KeyPair(certPEMBlock, keyPEMBlock []byte) (Certificate, error)
		// X509KeyPair parses a public/private key pair from a pair of PEM encoded data. On successful return, Certificate.Leaf will be nil because the parsed form of the certificate is not retained.
		certs, _ := tls.X509KeyPair(c.config.APIClientPath.Cert, c.config.APIClientPath.Key)

		// 微信支付 HTTPS 服务器证书的根证书 .pem 格式
		// rootCa, _ := ioutil.ReadFile(c.config.APIClientPath.Root)

		pool := x509.NewCertPool()
		pool.AppendCertsFromPEM(c.config.APIClientPath.Root)

		client = http.Client{Transport: &http.Transport{
			DisableKeepAlives: true,
			TLSClientConfig: &tls.Config{
				RootCAs:      pool,
				Certificates: []tls.Certificate{certs},
			},
		}}
	}

	if err := c.checkConfig(); err != nil {
		return nil, err
	}

	resp, err := client.Post(url, "", body)
	if err == nil {
		defer resp.Body.Close()
		b, _ := ioutil.ReadAll(resp.Body)
		var result PublicResponse
		_ = xml.Unmarshal(b, &result)
		err := result.ResultCheck()
		if err == nil {
			return utils.XML2MAP(b), nil
		}
		return nil, err
	}
	return nil, err
}

关于作者 🌱

我是来自山东烟台的一名开发者,有敢兴趣的话题,或者软件开发需求,欢迎加微信 zhongwei 聊聊, 查看更多联系方式