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