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

 

 

反馈
咨询
目录
置顶