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
文档是否有帮助