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