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

文章目录

    之前用 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 聊聊,或者关注我的个人公众号“大象工具”, 查看更多联系方式