package wechat import ( "context" "crypto" "crypto/rand" "crypto/rsa" "crypto/sha256" "encoding/base64" "encoding/json" "fmt" "net/http" "runtime" "sort" "sync" "time" "github.com/go-pay/gopay" "github.com/go-pay/gopay/pkg/aes" "github.com/go-pay/gopay/pkg/errgroup" "github.com/go-pay/gopay/pkg/util" "github.com/go-pay/gopay/pkg/xhttp" "github.com/go-pay/gopay/pkg/xlog" "github.com/go-pay/gopay/pkg/xpem" "github.com/go-pay/gopay/pkg/xtime" ) // 获取微信平台证书公钥(获取后自行保存使用,如需定期刷新功能,自行实现) // 注意事项 // 如果自行实现验证平台签名逻辑的话,需要注意以下事项: // - 程序实现定期更新平台证书的逻辑,不要硬编码验证应答消息签名的平台证书 // - 定期调用该接口,间隔时间小于12小时 // - 加密请求消息中的敏感信息时,使用最新的平台证书(即:证书启用时间较晚的证书) // 文档说明:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay5_1.shtml func GetPlatformCerts(ctx context.Context, mchid, apiV3Key, serialNo, privateKey string) (certs *PlatformCertRsp, err error) { var ( eg = new(errgroup.Group) mu sync.Mutex jb = "" ) // Prepare priKey, err := xpem.DecodePrivateKey([]byte(privateKey)) if err != nil { return nil, err } timestamp := time.Now().Unix() nonceStr := util.RandomString(32) ts := util.Int642String(timestamp) _str := MethodGet + "\n" + v3GetCerts + "\n" + ts + "\n" + nonceStr + "\n" + jb + "\n" // Sign h := sha256.New() h.Write([]byte(_str)) result, err := rsa.SignPKCS1v15(rand.Reader, priKey, crypto.SHA256, h.Sum(nil)) if err != nil { return nil, fmt.Errorf("[%w]: %+v", gopay.SignatureErr, err) } sign := base64.StdEncoding.EncodeToString(result) // Authorization authorization := Authorization + ` mchid="` + mchid + `",nonce_str="` + nonceStr + `",timestamp="` + ts + `",serial_no="` + serialNo + `",signature="` + sign + `"` // Request var url = v3BaseUrlCh + v3GetCerts httpClient := xhttp.NewClient() httpClient.Header.Add(HeaderAuthorization, authorization) httpClient.Header.Add(HeaderRequestID, fmt.Sprintf("%s-%d", util.RandomString(21), time.Now().Unix())) httpClient.Header.Add(HeaderSerial, serialNo) httpClient.Header.Add("Accept", "*/*") res, bs, err := httpClient.Type(xhttp.TypeJSON).Get(url).EndBytes(ctx) if err != nil { return nil, err } certs = &PlatformCertRsp{Code: Success} if res.StatusCode != http.StatusOK { certs.Code = res.StatusCode certs.Error = string(bs) return certs, nil } // Parse certRsp := new(PlatformCert) if err = json.Unmarshal(bs, certRsp); err != nil { return nil, fmt.Errorf("json.Unmarshal(%s):%+v", string(bs), err) } for _, v := range certRsp.Data { cert := v if cert.EncryptCertificate != nil { ec := cert.EncryptCertificate eg.Go(func(ctx context.Context) error { cipherBytes, _ := base64.StdEncoding.DecodeString(ec.Ciphertext) pubKeyBytes, err := aes.GCMDecrypt(cipherBytes, []byte(ec.Nonce), []byte(ec.AssociatedData), []byte(apiV3Key)) if err != nil { return fmt.Errorf("aes.GCMDecrypt, err:%+v", err) } pci := &PlatformCertItem{ EffectiveTime: cert.EffectiveTime, ExpireTime: cert.ExpireTime, PublicKey: string(pubKeyBytes), SerialNo: cert.SerialNo, } mu.Lock() certs.Certs = append(certs.Certs, pci) mu.Unlock() return nil }) } } if err = eg.Wait(); err != nil { return nil, err } return certs, nil } // 设置 微信支付平台证书 和 证书序列号 // 注意1:如已开启自动验签功能 client.AutoVerifySign(),无需再调用此方法设置 // 注意2:请预先通过 wechat.GetPlatformCerts() 获取 微信平台公钥证书 和 证书序列号 // 部分接口请求参数中敏感信息加密,使用此 微信支付平台公钥 和 证书序列号 func (c *ClientV3) SetPlatformCert(wxPublicKeyContent []byte, wxSerialNo string) (client *ClientV3) { pubKey, err := xpem.DecodePublicKey(wxPublicKeyContent) if err != nil { xlog.Errorf("SetPlatformCert(%s),err:%+v", wxPublicKeyContent, err) } if pubKey != nil { c.wxPublicKey = pubKey } c.WxSerialNo = wxSerialNo return c } // 获取 微信平台证书(readonly) func (c *ClientV3) WxPublicKey() (wxPublicKey *rsa.PublicKey) { wxPublicKey = c.wxPublicKey return } // 获取并选择最新的有效证书 func (c *ClientV3) GetAndSelectNewestCert() (cert, serialNo string, err error) { certs, err := c.getPlatformCerts() if err != nil { return gopay.NULL, gopay.NULL, err } if certs.Code == Success && len(certs.Certs) > 0 { // only one if len(certs.Certs) == 1 { formatExpire := xtime.FormatDateTime(certs.Certs[0].ExpireTime) expireTime, err := time.ParseInLocation(xtime.TimeLayout, formatExpire, time.Local) if err != nil { return gopay.NULL, gopay.NULL, fmt.Errorf("time.ParseInLocation(%s, %s),err:%w", xtime.TimeLayout, formatExpire, err) } if time.Now().Unix() >= expireTime.Unix() { // 过期了 return gopay.NULL, gopay.NULL, fmt.Errorf("wechat platform API cert expired, expired time: %s", formatExpire) } return certs.Certs[0].PublicKey, certs.Certs[0].SerialNo, nil } // more one var ( effectiveTs []int certMap = make(map[int]*PlatformCertItem) ) for _, v := range certs.Certs { formatEffective := xtime.FormatDateTime(v.EffectiveTime) effectiveTime, err := time.ParseInLocation(xtime.TimeLayout, formatEffective, time.Local) if err != nil { return gopay.NULL, gopay.NULL, fmt.Errorf("time.ParseInLocation(%s, %s),err:%w", xtime.TimeLayout, formatEffective, err) } eu := int(effectiveTime.Unix()) effectiveTs = append(effectiveTs, eu) certMap[eu] = v } sort.Ints(effectiveTs) // newest cert newestCert := certMap[effectiveTs[len(effectiveTs)-1]] formatExpire := xtime.FormatDateTime(newestCert.ExpireTime) expireTime, err := time.ParseInLocation(xtime.TimeLayout, formatExpire, time.Local) if err != nil { return gopay.NULL, gopay.NULL, fmt.Errorf("time.ParseInLocation(%s, %s),err:%w", xtime.TimeLayout, formatExpire, err) } if time.Now().Unix() >= expireTime.Unix() { // 过期了 return gopay.NULL, gopay.NULL, fmt.Errorf("wechat platform API cert expired, expired time: %s", formatExpire) } return newestCert.PublicKey, newestCert.SerialNo, nil } // failed return gopay.NULL, gopay.NULL, fmt.Errorf("GetAndSelectNewestCert() failed or certs is nil: %+v", certs) } // 推荐直接使用 client.GetAndSelectNewestCert() 方法 // 获取微信平台证书公钥(获取后自行保存使用,如需定期刷新功能,自行实现) // 注意事项 // 如果自行实现验证平台签名逻辑的话,需要注意以下事项: // - 程序实现定期更新平台证书的逻辑,不要硬编码验证应答消息签名的平台证书 // - 定期调用该接口,间隔时间小于12小时 // - 加密请求消息中的敏感信息时,使用最新的平台证书(即:证书启用时间较晚的证书) // 文档说明:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay5_1.shtml func (c *ClientV3) getPlatformCerts() (certs *PlatformCertRsp, err error) { var ( eg = new(errgroup.Group) mu sync.Mutex ) authorization, err := c.authorization(MethodGet, v3GetCerts, nil) if err != nil { return nil, err } res, _, bs, err := c.doProdGet(c.ctx, v3GetCerts, authorization) if err != nil { return nil, err } certs = &PlatformCertRsp{Code: Success} if res.StatusCode != http.StatusOK { certs.Code = res.StatusCode certs.Error = string(bs) return certs, nil } certRsp := new(PlatformCert) if err = json.Unmarshal(bs, certRsp); err != nil { return nil, fmt.Errorf("json.Unmarshal(%s):%+v", string(bs), err) } for _, v := range certRsp.Data { cert := v if cert.EncryptCertificate != nil { ec := cert.EncryptCertificate eg.Go(func(ctx context.Context) error { pubKey, err := c.decryptCerts(ec.Ciphertext, ec.Nonce, ec.AssociatedData) if err != nil { return err } pci := &PlatformCertItem{ EffectiveTime: cert.EffectiveTime, ExpireTime: cert.ExpireTime, PublicKey: pubKey, SerialNo: cert.SerialNo, } mu.Lock() certs.Certs = append(certs.Certs, pci) mu.Unlock() return nil }) } } if err = eg.Wait(); err != nil { return nil, err } return certs, nil } // 解密加密的证书 func (c *ClientV3) decryptCerts(ciphertext, nonce, additional string) (wxCerts string, err error) { cipherBytes, _ := base64.StdEncoding.DecodeString(ciphertext) decrypt, err := aes.GCMDecrypt(cipherBytes, []byte(nonce), []byte(additional), c.ApiV3Key) if err != nil { return "", fmt.Errorf("aes.GCMDecrypt, err:%w", err) } return string(decrypt), nil } func (c *ClientV3) autoCheckCertProc() { defer func() { if r := recover(); r != nil { buf := make([]byte, 64<<10) buf = buf[:runtime.Stack(buf, false)] xlog.Errorf("autoCheckCertProc: panic recovered: %s\n%s", r, buf) // 重启 c.autoCheckCertProc() } }() for { time.Sleep(time.Hour * 12) wxPk, wxSerialNo, err := c.GetAndSelectNewestCert() if err != nil { xlog.Errorf("c.GetAndSelectNewestCert(),err:%+v", err) continue } // decode cert pubKey, err := xpem.DecodePublicKey([]byte(wxPk)) if err != nil { xlog.Errorf("xpem.DecodePublicKey(%s),err:%+v", wxPk, err) continue } c.wxPublicKey = pubKey c.WxSerialNo = wxSerialNo } }