forked from golang/hotime
285 lines
9.3 KiB
Go
285 lines
9.3 KiB
Go
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
|
||
}
|
||
}
|