之前用 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 聊聊, 查看更多联系方式