Go

更新时间:2025.05.29

一、概述

本工具类 wxpay_utility 为使用 Go 接入微信支付的开发者提供了一系列实用的功能,包括 JSON 处理、密钥加载、加密签名、请求头构建、响应验证等。通过使用这个工具类,开发者可以更方便地完成与微信支付相关的开发工作。

二、环境要求

  • Go 1.16+

三、必需的证书和密钥

运行 SDK 必需以下的商户身份信息,用于构造请求的签名和验证应答的签名:

四、工具类代码

1package wxpay_utility
2
3import (
4	"bytes"
5	"crypto"
6	"crypto/rand"
7	"crypto/rsa"
8	"crypto/sha1"
9	"crypto/sha256"
10	"crypto/x509"
11	"encoding/base64"
12	"encoding/json"
13	"encoding/pem"
14	"errors"
15	"fmt"
16	"io"
17	"net/http"
18	"os"
19	"strconv"
20	"time"
21)
22
23// MchConfig 商户信息配置,用于调用商户API
24type MchConfig struct {
25	mchId                      string
26	certificateSerialNo        string
27	privateKeyFilePath         string
28	wechatPayPublicKeyId       string
29	wechatPayPublicKeyFilePath string
30	privateKey                 *rsa.PrivateKey
31	wechatPayPublicKey         *rsa.PublicKey
32}
33
34// MchId 商户号
35func (c *MchConfig) MchId() string {
36	return c.mchId
37}
38
39// CertificateSerialNo 商户API证书序列号
40func (c *MchConfig) CertificateSerialNo() string {
41	return c.certificateSerialNo
42}
43
44// PrivateKey 商户API证书对应的私钥
45func (c *MchConfig) PrivateKey() *rsa.PrivateKey {
46	return c.privateKey
47}
48
49// WechatPayPublicKeyId 微信支付公钥ID
50func (c *MchConfig) WechatPayPublicKeyId() string {
51	return c.wechatPayPublicKeyId
52}
53
54// WechatPayPublicKey 微信支付公钥
55func (c *MchConfig) WechatPayPublicKey() *rsa.PublicKey {
56	return c.wechatPayPublicKey
57}
58
59// CreateMchConfig MchConfig 构造函数
60func CreateMchConfig(
61	mchId string,
62	certificateSerialNo string,
63	privateKeyFilePath string,
64	wechatPayPublicKeyId string,
65	wechatPayPublicKeyFilePath string,
66) (*MchConfig, error) {
67	mchConfig := &MchConfig{
68		mchId:                      mchId,
69		certificateSerialNo:        certificateSerialNo,
70		privateKeyFilePath:         privateKeyFilePath,
71		wechatPayPublicKeyId:       wechatPayPublicKeyId,
72		wechatPayPublicKeyFilePath: wechatPayPublicKeyFilePath,
73	}
74	privateKey, err := LoadPrivateKeyWithPath(mchConfig.privateKeyFilePath)
75	if err != nil {
76		return nil, err
77	}
78	mchConfig.privateKey = privateKey
79	wechatPayPublicKey, err := LoadPublicKeyWithPath(mchConfig.wechatPayPublicKeyFilePath)
80	if err != nil {
81		return nil, err
82	}
83	mchConfig.wechatPayPublicKey = wechatPayPublicKey
84	return mchConfig, nil
85}
86
87// LoadPrivateKey 通过私钥的文本内容加载私钥
88func LoadPrivateKey(privateKeyStr string) (privateKey *rsa.PrivateKey, err error) {
89	block, _ := pem.Decode([]byte(privateKeyStr))
90	if block == nil {
91		return nil, fmt.Errorf("decode private key err")
92	}
93	if block.Type != "PRIVATE KEY" {
94		return nil, fmt.Errorf("the kind of PEM should be PRVATE KEY")
95	}
96	key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
97	if err != nil {
98		return nil, fmt.Errorf("parse private key err:%s", err.Error())
99	}
100	privateKey, ok := key.(*rsa.PrivateKey)
101	if !ok {
102		return nil, fmt.Errorf("not a RSA private key")
103	}
104	return privateKey, nil
105}
106
107// LoadPublicKey 通过公钥的文本内容加载公钥
108func LoadPublicKey(publicKeyStr string) (publicKey *rsa.PublicKey, err error) {
109	block, _ := pem.Decode([]byte(publicKeyStr))
110	if block == nil {
111		return nil, errors.New("decode public key error")
112	}
113	if block.Type != "PUBLIC KEY" {
114		return nil, fmt.Errorf("the kind of PEM should be PUBLIC KEY")
115	}
116	key, err := x509.ParsePKIXPublicKey(block.Bytes)
117	if err != nil {
118		return nil, fmt.Errorf("parse public key err:%s", err.Error())
119	}
120	publicKey, ok := key.(*rsa.PublicKey)
121	if !ok {
122		return nil, fmt.Errorf("%s is not rsa public key", publicKeyStr)
123	}
124	return publicKey, nil
125}
126
127// LoadPrivateKeyWithPath 通过私钥的文件路径内容加载私钥
128func LoadPrivateKeyWithPath(path string) (privateKey *rsa.PrivateKey, err error) {
129	privateKeyBytes, err := os.ReadFile(path)
130	if err != nil {
131		return nil, fmt.Errorf("read private pem file err:%s", err.Error())
132	}
133	return LoadPrivateKey(string(privateKeyBytes))
134}
135
136// LoadPublicKeyWithPath 通过公钥的文件路径加载公钥
137func LoadPublicKeyWithPath(path string) (publicKey *rsa.PublicKey, err error) {
138	publicKeyBytes, err := os.ReadFile(path)
139	if err != nil {
140		return nil, fmt.Errorf("read certificate pem file err:%s", err.Error())
141	}
142	return LoadPublicKey(string(publicKeyBytes))
143}
144
145// EncryptOAEPWithPublicKey 使用 OAEP padding方式用公钥进行加密
146func EncryptOAEPWithPublicKey(message string, publicKey *rsa.PublicKey) (ciphertext string, err error) {
147	if publicKey == nil {
148		return "", fmt.Errorf("you should input *rsa.PublicKey")
149	}
150	ciphertextByte, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, publicKey, []byte(message), nil)
151	if err != nil {
152		return "", fmt.Errorf("encrypt message with public key err:%s", err.Error())
153	}
154	ciphertext = base64.StdEncoding.EncodeToString(ciphertextByte)
155	return ciphertext, nil
156}
157
158// SignSHA256WithRSA 通过私钥对字符串以 SHA256WithRSA 算法生成签名信息
159func SignSHA256WithRSA(source string, privateKey *rsa.PrivateKey) (signature string, err error) {
160	if privateKey == nil {
161		return "", fmt.Errorf("private key should not be nil")
162	}
163	h := crypto.Hash.New(crypto.SHA256)
164	_, err = h.Write([]byte(source))
165	if err != nil {
166		return "", nil
167	}
168	hashed := h.Sum(nil)
169	signatureByte, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed)
170	if err != nil {
171		return "", err
172	}
173	return base64.StdEncoding.EncodeToString(signatureByte), nil
174}
175
176// VerifySHA256WithRSA 通过公钥对字符串和签名结果以 SHA256WithRSA 验证签名有效性
177func VerifySHA256WithRSA(source string, signature string, publicKey *rsa.PublicKey) error {
178	if publicKey == nil {
179		return fmt.Errorf("public key should not be nil")
180	}
181
182	sigBytes, err := base64.StdEncoding.DecodeString(signature)
183	if err != nil {
184		return fmt.Errorf("verify failed: signature is not base64 encoded")
185	}
186	hashed := sha256.Sum256([]byte(source))
187	err = rsa.VerifyPKCS1v15(publicKey, crypto.SHA256, hashed[:], sigBytes)
188	if err != nil {
189		return fmt.Errorf("verify signature with public key error:%s", err.Error())
190	}
191	return nil
192}
193
194// GenerateNonce 生成一个长度为 NonceLength 的随机字符串(只包含大小写字母与数字)
195func GenerateNonce() (string, error) {
196	const (
197		// NonceSymbols 随机字符串可用字符集
198		NonceSymbols = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
199		// NonceLength 随机字符串的长度
200		NonceLength = 32
201	)
202
203	bytes := make([]byte, NonceLength)
204	_, err := rand.Read(bytes)
205	if err != nil {
206		return "", err
207	}
208	symbolsByteLength := byte(len(NonceSymbols))
209	for i, b := range bytes {
210		bytes[i] = NonceSymbols[b%symbolsByteLength]
211	}
212	return string(bytes), nil
213}
214
215// BuildAuthorization 构建请求头中的 Authorization 信息
216func BuildAuthorization(
217	mchid string,
218	certificateSerialNo string,
219	privateKey *rsa.PrivateKey,
220	method string,
221	canonicalURL string,
222	body []byte,
223) (string, error) {
224	const (
225		SignatureMessageFormat = "%s\n%s\n%d\n%s\n%s\n" // 数字签名原文格式
226		// HeaderAuthorizationFormat 请求头中的 Authorization 拼接格式
227		HeaderAuthorizationFormat = "WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",timestamp=\"%d\",serial_no=\"%s\",signature=\"%s\""
228	)
229
230	nonce, err := GenerateNonce()
231	if err != nil {
232		return "", err
233	}
234	timestamp := time.Now().Unix()
235	message := fmt.Sprintf(SignatureMessageFormat, method, canonicalURL, timestamp, nonce, body)
236	signature, err := SignSHA256WithRSA(message, privateKey)
237	if err != nil {
238		return "", err
239	}
240	authorization := fmt.Sprintf(
241		HeaderAuthorizationFormat,
242		mchid, nonce, timestamp, certificateSerialNo, signature,
243	)
244	return authorization, nil
245}
246
247// ExtractResponseBody 提取应答报文的 Body
248func ExtractResponseBody(response *http.Response) ([]byte, error) {
249	if response.Body == nil {
250		return nil, nil
251	}
252
253	body, err := io.ReadAll(response.Body)
254	if err != nil {
255		return nil, fmt.Errorf("read response body err:[%s]", err.Error())
256	}
257	response.Body = io.NopCloser(bytes.NewBuffer(body))
258	return body, nil
259}
260
261const (
262	WechatPayTimestamp = "Wechatpay-Timestamp" // 微信支付回包时间戳
263	WechatPayNonce     = "Wechatpay-Nonce"     // 微信支付回包随机字符串
264	WechatPaySignature = "Wechatpay-Signature" // 微信支付回包签名信息
265	WechatPaySerial    = "Wechatpay-Serial"    // 微信支付回包平台序列号
266	RequestID          = "Request-Id"          // 微信支付回包请求ID
267)
268
269// ValidateResponse 验证微信支付回包的签名信息
270func ValidateResponse(
271	wechatpayPublicKeyId string,
272	wechatpayPublicKey *rsa.PublicKey,
273	headers *http.Header,
274	body []byte,
275) error {
276	requestID := headers.Get(RequestID)
277	timestampStr := headers.Get(WechatPayTimestamp)
278	serialNo := headers.Get(WechatPaySerial)
279	signature := headers.Get(WechatPaySignature)
280	nonce := headers.Get(WechatPayNonce)
281
282	// 拒绝过期请求
283	timestamp, err := strconv.ParseInt(timestampStr, 10, 64)
284	if err != nil {
285		return fmt.Errorf("invalid timestamp: %v", err)
286	}
287	if time.Now().Sub(time.Unix(timestamp, 0)) > 5*time.Minute {
288		return errors.New("invalid timestamp")
289	}
290
291	if serialNo != wechatpayPublicKeyId {
292		return fmt.Errorf(
293			"serial-no mismatch: got %s, expected %s, request-id: %s",
294			serialNo,
295			wechatpayPublicKeyId,
296			requestID,
297		)
298	}
299
300	message := fmt.Sprintf("%s\n%s\n%s\n", timestampStr, nonce, body)
301	if err := VerifySHA256WithRSA(message, signature, wechatpayPublicKey); err != nil {
302		return fmt.Errorf("invalid signature: %v, request-id: %s", err, requestID)
303	}
304
305	return nil
306}
307
308// ApiException 微信支付API错误异常,发送HTTP请求成功,但返回状态码不是 2XX 时抛出本异常
309type ApiException struct {
310	statusCode   int         // 应答报文的 HTTP 状态码
311	header       http.Header // 应答报文的 Header 信息
312	body         []byte      // 应答报文的 Body 原文
313	errorCode    string      // 微信支付回包的错误码
314	errorMessage string      // 微信支付回包的错误信息
315}
316
317func (c *ApiException) Error() string {
318	buf := bytes.NewBuffer(nil)
319	buf.WriteString(fmt.Sprintf("api error:[StatusCode: %d, Body: %s", c.statusCode, string(c.body)))
320	if len(c.header) > 0 {
321		buf.WriteString(" Header: ")
322		for key, value := range c.header {
323			buf.WriteString(fmt.Sprintf("\n - %v=%v", key, value))
324		}
325		buf.WriteString("\n")
326	}
327	buf.WriteString("]")
328	return buf.String()
329}
330
331func (c *ApiException) StatusCode() int {
332	return c.statusCode
333}
334
335func (c *ApiException) Header() http.Header {
336	return c.header
337}
338
339func (c *ApiException) Body() []byte {
340	return c.body
341}
342
343func (c *ApiException) ErrorCode() string {
344	return c.errorCode
345}
346
347func (c *ApiException) ErrorMessage() string {
348	return c.errorMessage
349}
350
351func NewApiException(statusCode int, header http.Header, body []byte) error {
352	ret := &ApiException{
353		statusCode: statusCode,
354		header:     header,
355		body:       body,
356	}
357
358	bodyObject := map[string]interface{}{}
359	if err := json.Unmarshal(body, &bodyObject); err == nil {
360		if val, ok := bodyObject["code"]; ok {
361			ret.errorCode = val.(string)
362		}
363		if val, ok := bodyObject["message"]; ok {
364			ret.errorMessage = val.(string)
365		}
366	}
367
368	return ret
369}
370
371// Time 复制 time.Time 对象,并返回复制体的指针
372func Time(t time.Time) *time.Time {
373	return &t
374}
375
376// String 复制 string 对象,并返回复制体的指针
377func String(s string) *string {
378	return &s
379}
380
381// Bool 复制 bool 对象,并返回复制体的指针
382func Bool(b bool) *bool {
383	return &b
384}
385
386// Float64 复制 float64 对象,并返回复制体的指针
387func Float64(f float64) *float64 {
388	return &f
389}
390
391// Float32 复制 float32 对象,并返回复制体的指针
392func Float32(f float32) *float32 {
393	return &f
394}
395
396// Int64 复制 int64 对象,并返回复制体的指针
397func Int64(i int64) *int64 {
398	return &i
399}
400
401// Int32 复制 int64 对象,并返回复制体的指针
402func Int32(i int32) *int32 {
403	return &i
404}

 

 

反馈
咨询
目录
置顶