Go

更新时间:2025.05.29

一、概述

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

二、环境要求

  • Go 1.16+

三、必需的证书和密钥

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

四、工具类代码

1package wxpay_utility
2
3import (
4	"bytes"
5	"crypto"
6	"crypto/aes"
7	"crypto/cipher"
8	"crypto/rand"
9	"crypto/rsa"
10	"crypto/sha1"
11	"crypto/sha256"
12	"crypto/x509"
13	"encoding/base64"
14	"encoding/json"
15	"encoding/pem"
16	"errors"
17	"fmt"
18	"hash"
19	"io"
20	"net/http"
21	"os"
22	"strconv"
23	"time"
24)
25
26// MchConfig 商户信息配置,用于调用商户API
27type MchConfig struct {
28	mchId                      string
29	certificateSerialNo        string
30	privateKeyFilePath         string
31	wechatPayPublicKeyId       string
32	wechatPayPublicKeyFilePath string
33	privateKey                 *rsa.PrivateKey
34	wechatPayPublicKey         *rsa.PublicKey
35}
36
37// MchId 商户号
38func (c *MchConfig) MchId() string {
39	return c.mchId
40}
41
42// CertificateSerialNo 商户API证书序列号
43func (c *MchConfig) CertificateSerialNo() string {
44	return c.certificateSerialNo
45}
46
47// PrivateKey 商户API证书对应的私钥
48func (c *MchConfig) PrivateKey() *rsa.PrivateKey {
49	return c.privateKey
50}
51
52// WechatPayPublicKeyId 微信支付公钥ID
53func (c *MchConfig) WechatPayPublicKeyId() string {
54	return c.wechatPayPublicKeyId
55}
56
57// WechatPayPublicKey 微信支付公钥
58func (c *MchConfig) WechatPayPublicKey() *rsa.PublicKey {
59	return c.wechatPayPublicKey
60}
61
62// CreateMchConfig MchConfig 构造函数
63func CreateMchConfig(
64	mchId string,
65	certificateSerialNo string,
66	privateKeyFilePath string,
67	wechatPayPublicKeyId string,
68	wechatPayPublicKeyFilePath string,
69) (*MchConfig, error) {
70	mchConfig := &MchConfig{
71		mchId:                      mchId,
72		certificateSerialNo:        certificateSerialNo,
73		privateKeyFilePath:         privateKeyFilePath,
74		wechatPayPublicKeyId:       wechatPayPublicKeyId,
75		wechatPayPublicKeyFilePath: wechatPayPublicKeyFilePath,
76	}
77	privateKey, err := LoadPrivateKeyWithPath(mchConfig.privateKeyFilePath)
78	if err != nil {
79		return nil, err
80	}
81	mchConfig.privateKey = privateKey
82	wechatPayPublicKey, err := LoadPublicKeyWithPath(mchConfig.wechatPayPublicKeyFilePath)
83	if err != nil {
84		return nil, err
85	}
86	mchConfig.wechatPayPublicKey = wechatPayPublicKey
87	return mchConfig, nil
88}
89
90// LoadPrivateKey 通过私钥的文本内容加载私钥
91func LoadPrivateKey(privateKeyStr string) (privateKey *rsa.PrivateKey, err error) {
92	block, _ := pem.Decode([]byte(privateKeyStr))
93	if block == nil {
94		return nil, fmt.Errorf("decode private key err")
95	}
96	if block.Type != "PRIVATE KEY" {
97		return nil, fmt.Errorf("the kind of PEM should be PRVATE KEY")
98	}
99	key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
100	if err != nil {
101		return nil, fmt.Errorf("parse private key err:%s", err.Error())
102	}
103	privateKey, ok := key.(*rsa.PrivateKey)
104	if !ok {
105		return nil, fmt.Errorf("not a RSA private key")
106	}
107	return privateKey, nil
108}
109
110// LoadPublicKey 通过公钥的文本内容加载公钥
111func LoadPublicKey(publicKeyStr string) (publicKey *rsa.PublicKey, err error) {
112	block, _ := pem.Decode([]byte(publicKeyStr))
113	if block == nil {
114		return nil, errors.New("decode public key error")
115	}
116	if block.Type != "PUBLIC KEY" {
117		return nil, fmt.Errorf("the kind of PEM should be PUBLIC KEY")
118	}
119	key, err := x509.ParsePKIXPublicKey(block.Bytes)
120	if err != nil {
121		return nil, fmt.Errorf("parse public key err:%s", err.Error())
122	}
123	publicKey, ok := key.(*rsa.PublicKey)
124	if !ok {
125		return nil, fmt.Errorf("%s is not rsa public key", publicKeyStr)
126	}
127	return publicKey, nil
128}
129
130// LoadPrivateKeyWithPath 通过私钥的文件路径内容加载私钥
131func LoadPrivateKeyWithPath(path string) (privateKey *rsa.PrivateKey, err error) {
132	privateKeyBytes, err := os.ReadFile(path)
133	if err != nil {
134		return nil, fmt.Errorf("read private pem file err:%s", err.Error())
135	}
136	return LoadPrivateKey(string(privateKeyBytes))
137}
138
139// LoadPublicKeyWithPath 通过公钥的文件路径加载公钥
140func LoadPublicKeyWithPath(path string) (publicKey *rsa.PublicKey, err error) {
141	publicKeyBytes, err := os.ReadFile(path)
142	if err != nil {
143		return nil, fmt.Errorf("read certificate pem file err:%s", err.Error())
144	}
145	return LoadPublicKey(string(publicKeyBytes))
146}
147
148// EncryptOAEPWithPublicKey 使用 OAEP padding方式用公钥进行加密
149func EncryptOAEPWithPublicKey(message string, publicKey *rsa.PublicKey) (ciphertext string, err error) {
150	if publicKey == nil {
151		return "", fmt.Errorf("you should input *rsa.PublicKey")
152	}
153	ciphertextByte, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, publicKey, []byte(message), nil)
154	if err != nil {
155		return "", fmt.Errorf("encrypt message with public key err:%s", err.Error())
156	}
157	ciphertext = base64.StdEncoding.EncodeToString(ciphertextByte)
158	return ciphertext, nil
159}
160
161// DecryptAES256GCM 使用 AEAD_AES_256_GCM 算法进行解密
162//
163// 可以使用此算法完成微信支付回调报文解密
164func DecryptAES256GCM(aesKey, associatedData, nonce, ciphertext string) (plaintext string, err error) {
165	decodedCiphertext, err := base64.StdEncoding.DecodeString(ciphertext)
166	if err != nil {
167		return "", err
168	}
169	c, err := aes.NewCipher([]byte(aesKey))
170	if err != nil {
171		return "", err
172	}
173	gcm, err := cipher.NewGCM(c)
174	if err != nil {
175		return "", err
176	}
177	dataBytes, err := gcm.Open(nil, []byte(nonce), decodedCiphertext, []byte(associatedData))
178	if err != nil {
179		return "", err
180	}
181	return string(dataBytes), nil
182}
183
184// SignSHA256WithRSA 通过私钥对字符串以 SHA256WithRSA 算法生成签名信息
185func SignSHA256WithRSA(source string, privateKey *rsa.PrivateKey) (signature string, err error) {
186	if privateKey == nil {
187		return "", fmt.Errorf("private key should not be nil")
188	}
189	h := crypto.Hash.New(crypto.SHA256)
190	_, err = h.Write([]byte(source))
191	if err != nil {
192		return "", nil
193	}
194	hashed := h.Sum(nil)
195	signatureByte, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed)
196	if err != nil {
197		return "", err
198	}
199	return base64.StdEncoding.EncodeToString(signatureByte), nil
200}
201
202// VerifySHA256WithRSA 通过公钥对字符串和签名结果以 SHA256WithRSA 验证签名有效性
203func VerifySHA256WithRSA(source string, signature string, publicKey *rsa.PublicKey) error {
204	if publicKey == nil {
205		return fmt.Errorf("public key should not be nil")
206	}
207
208	sigBytes, err := base64.StdEncoding.DecodeString(signature)
209	if err != nil {
210		return fmt.Errorf("verify failed: signature is not base64 encoded")
211	}
212	hashed := sha256.Sum256([]byte(source))
213	err = rsa.VerifyPKCS1v15(publicKey, crypto.SHA256, hashed[:], sigBytes)
214	if err != nil {
215		return fmt.Errorf("verify signature with public key error:%s", err.Error())
216	}
217	return nil
218}
219
220// GenerateNonce 生成一个长度为 NonceLength 的随机字符串(只包含大小写字母与数字)
221func GenerateNonce() (string, error) {
222	const (
223		// NonceSymbols 随机字符串可用字符集
224		NonceSymbols = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
225		// NonceLength 随机字符串的长度
226		NonceLength = 32
227	)
228
229	bytes := make([]byte, NonceLength)
230	_, err := rand.Read(bytes)
231	if err != nil {
232		return "", err
233	}
234	symbolsByteLength := byte(len(NonceSymbols))
235	for i, b := range bytes {
236		bytes[i] = NonceSymbols[b%symbolsByteLength]
237	}
238	return string(bytes), nil
239}
240
241// BuildAuthorization 构建请求头中的 Authorization 信息
242func BuildAuthorization(
243	mchid string,
244	certificateSerialNo string,
245	privateKey *rsa.PrivateKey,
246	method string,
247	canonicalURL string,
248	body []byte,
249) (string, error) {
250	const (
251		SignatureMessageFormat = "%s\n%s\n%d\n%s\n%s\n" // 数字签名原文格式
252		// HeaderAuthorizationFormat 请求头中的 Authorization 拼接格式
253		HeaderAuthorizationFormat = "WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",timestamp=\"%d\",serial_no=\"%s\",signature=\"%s\""
254	)
255
256	nonce, err := GenerateNonce()
257	if err != nil {
258		return "", err
259	}
260	timestamp := time.Now().Unix()
261	message := fmt.Sprintf(SignatureMessageFormat, method, canonicalURL, timestamp, nonce, body)
262	signature, err := SignSHA256WithRSA(message, privateKey)
263	if err != nil {
264		return "", err
265	}
266	authorization := fmt.Sprintf(
267		HeaderAuthorizationFormat,
268		mchid, nonce, timestamp, certificateSerialNo, signature,
269	)
270	return authorization, nil
271}
272
273// ExtractResponseBody 提取应答报文的 Body
274func ExtractResponseBody(response *http.Response) ([]byte, error) {
275	if response.Body == nil {
276		return nil, nil
277	}
278
279	body, err := io.ReadAll(response.Body)
280	if err != nil {
281		return nil, fmt.Errorf("read response body err:[%s]", err.Error())
282	}
283	response.Body = io.NopCloser(bytes.NewBuffer(body))
284	return body, nil
285}
286
287const (
288	WechatPayTimestamp = "Wechatpay-Timestamp" // 微信支付回包时间戳
289	WechatPayNonce     = "Wechatpay-Nonce"     // 微信支付回包随机字符串
290	WechatPaySignature = "Wechatpay-Signature" // 微信支付回包签名信息
291	WechatPaySerial    = "Wechatpay-Serial"    // 微信支付回包平台序列号
292	RequestID          = "Request-Id"          // 微信支付回包请求ID
293)
294
295func validateWechatPaySignature(
296	wechatpayPublicKeyId string,
297	wechatpayPublicKey *rsa.PublicKey,
298	headers *http.Header,
299	body []byte,
300) error {
301	timestampStr := headers.Get(WechatPayTimestamp)
302	serialNo := headers.Get(WechatPaySerial)
303	signature := headers.Get(WechatPaySignature)
304	nonce := headers.Get(WechatPayNonce)
305
306	// 拒绝过期请求
307	timestamp, err := strconv.ParseInt(timestampStr, 10, 64)
308	if err != nil {
309		return fmt.Errorf("invalid timestamp: %w", err)
310	}
311	if time.Now().Sub(time.Unix(timestamp, 0)) > 5*time.Minute {
312		return fmt.Errorf("timestamp expired: %d", timestamp)
313	}
314
315	if serialNo != wechatpayPublicKeyId {
316		return fmt.Errorf(
317			"serial-no mismatch: got %s, expected %s",
318			serialNo,
319			wechatpayPublicKeyId,
320		)
321	}
322
323	message := fmt.Sprintf("%s\n%s\n%s\n", timestampStr, nonce, body)
324	if err := VerifySHA256WithRSA(message, signature, wechatpayPublicKey); err != nil {
325		return fmt.Errorf("invalid signature: %v", err)
326	}
327
328	return nil
329}
330
331// ValidateResponse 验证微信支付回包的签名信息
332func ValidateResponse(
333	wechatpayPublicKeyId string,
334	wechatpayPublicKey *rsa.PublicKey,
335	headers *http.Header,
336	body []byte,
337) error {
338	if err := validateWechatPaySignature(wechatpayPublicKeyId, wechatpayPublicKey, headers, body); err != nil {
339		return fmt.Errorf("validate response err: %w, RequestID: %s", err, headers.Get(RequestID))
340	}
341	return nil
342}
343
344func validateNotification(
345	wechatpayPublicKeyId string,
346	wechatpayPublicKey *rsa.PublicKey,
347	headers *http.Header,
348	body []byte,
349) error {
350	if err := validateWechatPaySignature(wechatpayPublicKeyId, wechatpayPublicKey, headers, body); err != nil {
351		return fmt.Errorf("validate notification err: %w", err)
352	}
353	return nil
354}
355
356// Resource 微信支付通知请求中的资源数据
357type Resource struct {
358	Algorithm      string `json:"algorithm"`
359	Ciphertext     string `json:"ciphertext"`
360	AssociatedData string `json:"associated_data"`
361	Nonce          string `json:"nonce"`
362	OriginalType   string `json:"original_type"`
363}
364
365// Notification 微信支付通知的数据结构
366type Notification struct {
367	ID           string     `json:"id"`
368	CreateTime   *time.Time `json:"create_time"`
369	EventType    string     `json:"event_type"`
370	ResourceType string     `json:"resource_type"`
371	Resource     *Resource  `json:"resource"`
372	Summary      string     `json:"summary"`
373
374	Plaintext string // 解密后的业务数据(JSON字符串)
375}
376
377func (c *Notification) validate() error {
378	if c.Resource == nil {
379		return errors.New("resource is nil")
380	}
381
382	if c.Resource.Algorithm != "AEAD_AES_256_GCM" {
383		return fmt.Errorf("unsupported algorithm: %s", c.Resource.Algorithm)
384	}
385
386	if c.Resource.Ciphertext == "" {
387		return errors.New("ciphertext is empty")
388	}
389
390	if c.Resource.AssociatedData == "" {
391		return errors.New("associated_data is empty")
392	}
393
394	if c.Resource.Nonce == "" {
395		return errors.New("nonce is empty")
396	}
397
398	if c.Resource.OriginalType == "" {
399		return fmt.Errorf("original_type is empty")
400	}
401
402	return nil
403}
404
405func (c *Notification) decrypt(apiv3Key string) error {
406	if err := c.validate(); err != nil {
407		return fmt.Errorf("notification format err: %w", err)
408	}
409
410	plaintext, err := DecryptAES256GCM(
411		apiv3Key,
412		c.Resource.AssociatedData,
413		c.Resource.Nonce,
414		c.Resource.Ciphertext,
415	)
416	if err != nil {
417		return fmt.Errorf("notification decrypt err: %w", err)
418	}
419
420	c.Plaintext = plaintext
421	return nil
422}
423
424// ParseNotification 解析微信支付通知的报文,返回通知中的业务数据
425// Notification.PlainText 为解密后的业务数据JSON字符串,请自行反序列化后使用
426func ParseNotification(
427	wechatpayPublicKeyId string,
428	wechatpayPublicKey *rsa.PublicKey,
429	apiv3Key string,
430	headers *http.Header,
431	body []byte,
432) (*Notification, error) {
433	if err := validateNotification(wechatpayPublicKeyId, wechatpayPublicKey, headers, body); err != nil {
434		return nil, err
435	}
436
437	notification := &Notification{}
438	if err := json.Unmarshal(body, notification); err != nil {
439		return nil, fmt.Errorf("parse notification err: %w", err)
440	}
441
442	if err := notification.decrypt(apiv3Key); err != nil {
443		return nil, fmt.Errorf("notification decrypt err: %w", err)
444	}
445
446	return notification, nil
447}
448
449// ApiException 微信支付API错误异常,发送HTTP请求成功,但返回状态码不是 2XX 时抛出本异常
450type ApiException struct {
451	statusCode   int         // 应答报文的 HTTP 状态码
452	header       http.Header // 应答报文的 Header 信息
453	body         []byte      // 应答报文的 Body 原文
454	errorCode    string      // 微信支付回包的错误码
455	errorMessage string      // 微信支付回包的错误信息
456}
457
458func (c *ApiException) Error() string {
459	buf := bytes.NewBuffer(nil)
460	buf.WriteString(fmt.Sprintf("api error:[StatusCode: %d, Body: %s", c.statusCode, string(c.body)))
461	if len(c.header) > 0 {
462		buf.WriteString(" Header: ")
463		for key, value := range c.header {
464			buf.WriteString(fmt.Sprintf("\n - %v=%v", key, value))
465		}
466		buf.WriteString("\n")
467	}
468	buf.WriteString("]")
469	return buf.String()
470}
471
472func (c *ApiException) StatusCode() int {
473	return c.statusCode
474}
475
476func (c *ApiException) Header() http.Header {
477	return c.header
478}
479
480func (c *ApiException) Body() []byte {
481	return c.body
482}
483
484func (c *ApiException) ErrorCode() string {
485	return c.errorCode
486}
487
488func (c *ApiException) ErrorMessage() string {
489	return c.errorMessage
490}
491
492func NewApiException(statusCode int, header http.Header, body []byte) error {
493	ret := &ApiException{
494		statusCode: statusCode,
495		header:     header,
496		body:       body,
497	}
498
499	bodyObject := map[string]interface{}{}
500	if err := json.Unmarshal(body, &bodyObject); err == nil {
501		if val, ok := bodyObject["code"]; ok {
502			ret.errorCode = val.(string)
503		}
504		if val, ok := bodyObject["message"]; ok {
505			ret.errorMessage = val.(string)
506		}
507	}
508
509	return ret
510}
511
512// Time 复制 time.Time 对象,并返回复制体的指针
513func Time(t time.Time) *time.Time {
514	return &t
515}
516
517// String 复制 string 对象,并返回复制体的指针
518func String(s string) *string {
519	return &s
520}
521
522// Bytes 复制 []byte 对象,并返回复制体的指针
523func Bytes(b []byte) *[]byte {
524	return &b
525}
526
527// Bool 复制 bool 对象,并返回复制体的指针
528func Bool(b bool) *bool {
529	return &b
530}
531
532// Float64 复制 float64 对象,并返回复制体的指针
533func Float64(f float64) *float64 {
534	return &f
535}
536
537// Float32 复制 float32 对象,并返回复制体的指针
538func Float32(f float32) *float32 {
539	return &f
540}
541
542// Int64 复制 int64 对象,并返回复制体的指针
543func Int64(i int64) *int64 {
544	return &i
545}
546
547// Int32 复制 int64 对象,并返回复制体的指针
548func Int32(i int32) *int32 {
549	return &i
550}
551
552// generateHashFromStream 从io.Reader流中生成哈希值的通用函数
553func generateHashFromStream(reader io.Reader, hashFunc func() hash.Hash, algorithmName string) (string, error) {
554	hash := hashFunc()
555	if _, err := io.Copy(hash, reader); err != nil {
556		return "", fmt.Errorf("failed to read stream for %s: %w", algorithmName, err)
557	}
558	return fmt.Sprintf("%x", hash.Sum(nil)), nil
559}
560
561// GenerateSHA256FromStream 从io.Reader流中生成SHA256哈希值
562func GenerateSHA256FromStream(reader io.Reader) (string, error) {
563	return generateHashFromStream(reader, sha256.New, "SHA256")
564}
565
566// GenerateSHA1FromStream 从io.Reader流中生成SHA1哈希值
567func GenerateSHA1FromStream(reader io.Reader) (string, error) {
568	return generateHashFromStream(reader, sha1.New, "SHA1")
569}
570