voucher/internal/pkg/wechat/utils/wxpay_utility.go

647 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package utils
import (
"bytes"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"sort"
"strconv"
"strings"
"time"
)
const (
Host = "https://api.mch.weixin.qq.com"
MethodGET = "GET"
MethodPOST = "POST"
)
// MchConfig 商户信息配置用于调用商户API
// https://pay.weixin.qq.com/doc/v3/merchant/4012716434
//
// 引用微信支付工具库 参考:https://pay.weixin.qq.com/doc/v3/merchant/4015119334
type MchConfig struct {
mchId string
certificateSerialNo string
privateKeyFilePath string
wechatPayPublicKeyId string
wechatPayPublicKeyFilePath string
privateKey *rsa.PrivateKey
wechatPayPublicKey *rsa.PublicKey
aesKey string
}
// MchId 商户号
func (c *MchConfig) MchId() string {
return c.mchId
}
// CertificateSerialNo 商户API证书序列号
func (c *MchConfig) CertificateSerialNo() string {
return c.certificateSerialNo
}
// PrivateKey 商户API证书对应的私钥
func (c *MchConfig) PrivateKey() *rsa.PrivateKey {
return c.privateKey
}
// WechatPayPublicKeyId 微信支付公钥ID
func (c *MchConfig) WechatPayPublicKeyId() string {
return c.wechatPayPublicKeyId
}
// WechatPayPublicKey 微信支付公钥
func (c *MchConfig) WechatPayPublicKey() *rsa.PublicKey {
return c.wechatPayPublicKey
}
// CreateMchConfig MchConfig 构造函数
func CreateMchConfig(
mchId string,
certificateSerialNo string,
privateKeyFilePath string,
wechatPayPublicKeyId string,
wechatPayPublicKeyFilePath string,
aesKey string,
) (*MchConfig, error) {
mchConfig := &MchConfig{
mchId: mchId,
certificateSerialNo: certificateSerialNo,
privateKeyFilePath: privateKeyFilePath,
wechatPayPublicKeyId: wechatPayPublicKeyId,
wechatPayPublicKeyFilePath: wechatPayPublicKeyFilePath,
aesKey: aesKey,
}
privateKey, err := LoadPrivateKeyWithPath(mchConfig.privateKeyFilePath)
if err != nil {
return nil, err
}
mchConfig.privateKey = privateKey
wechatPayPublicKey, err := LoadPublicKeyWithPath(mchConfig.wechatPayPublicKeyFilePath)
if err != nil {
return nil, err
}
mchConfig.wechatPayPublicKey = wechatPayPublicKey
return mchConfig, nil
}
// LoadPrivateKey 通过私钥的文本内容加载私钥
func LoadPrivateKey(privateKeyStr string) (privateKey *rsa.PrivateKey, err error) {
block, _ := pem.Decode([]byte(privateKeyStr))
if block == nil {
return nil, fmt.Errorf("decode private key err")
}
if block.Type != "PRIVATE KEY" {
return nil, fmt.Errorf("the kind of PEM should be PRVATE KEY")
}
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("parse private key err:%s", err.Error())
}
privateKey, ok := key.(*rsa.PrivateKey)
if !ok {
return nil, fmt.Errorf("not a RSA private key")
}
return privateKey, nil
}
// LoadPublicKey 通过公钥的文本内容加载公钥
func LoadPublicKey(publicKeyStr string) (publicKey *rsa.PublicKey, err error) {
block, _ := pem.Decode([]byte(publicKeyStr))
if block == nil {
return nil, errors.New("decode public key error")
}
if block.Type != "PUBLIC KEY" {
return nil, fmt.Errorf("the kind of PEM should be PUBLIC KEY")
}
key, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("parse public key err:%s", err.Error())
}
publicKey, ok := key.(*rsa.PublicKey)
if !ok {
return nil, fmt.Errorf("%s is not rsa public key", publicKeyStr)
}
return publicKey, nil
}
// LoadPrivateKeyWithPath 通过私钥的文件路径内容加载私钥
func LoadPrivateKeyWithPath(path string) (privateKey *rsa.PrivateKey, err error) {
privateKeyBytes, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read private pem file err:%s", err.Error())
}
return LoadPrivateKey(string(privateKeyBytes))
}
// LoadPublicKeyWithPath 通过公钥的文件路径加载公钥
func LoadPublicKeyWithPath(path string) (publicKey *rsa.PublicKey, err error) {
publicKeyBytes, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read certificate pem file err:%s", err.Error())
}
return LoadPublicKey(string(publicKeyBytes))
}
// EncryptOAEPWithPublicKey 使用 OAEP padding方式用公钥进行加密
func EncryptOAEPWithPublicKey(message string, publicKey *rsa.PublicKey) (ciphertext string, err error) {
if publicKey == nil {
return "", fmt.Errorf("you should input *rsa.PublicKey")
}
ciphertextByte, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, publicKey, []byte(message), nil)
if err != nil {
return "", fmt.Errorf("encrypt message with public key err:%s", err.Error())
}
ciphertext = base64.StdEncoding.EncodeToString(ciphertextByte)
return ciphertext, nil
}
// SignSHA256WithRSA 通过私钥对字符串以 SHA256WithRSA 算法生成签名信息
func SignSHA256WithRSA(source string, privateKey *rsa.PrivateKey) (signature string, err error) {
if privateKey == nil {
return "", fmt.Errorf("private key should not be nil")
}
h := crypto.Hash.New(crypto.SHA256)
_, err = h.Write([]byte(source))
if err != nil {
return "", nil
}
hashed := h.Sum(nil)
signatureByte, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(signatureByte), nil
}
// VerifySHA256WithRSA 通过公钥对字符串和签名结果以 SHA256WithRSA 验证签名有效性
func VerifySHA256WithRSA(source string, signature string, publicKey *rsa.PublicKey) error {
if publicKey == nil {
return fmt.Errorf("public key should not be nil")
}
sigBytes, err := base64.StdEncoding.DecodeString(signature)
if err != nil {
return fmt.Errorf("verify failed: signature is not base64 encoded")
}
hashed := sha256.Sum256([]byte(source))
err = rsa.VerifyPKCS1v15(publicKey, crypto.SHA256, hashed[:], sigBytes)
if err != nil {
return fmt.Errorf("verify signature with public key error:%s", err.Error())
}
return nil
}
// GenerateNonce 生成一个长度为 NonceLength 的随机字符串(只包含大小写字母与数字)
func GenerateNonce() (string, error) {
const (
// NonceSymbols 随机字符串可用字符集
NonceSymbols = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
// NonceLength 随机字符串的长度
NonceLength = 32
)
bs := make([]byte, NonceLength)
_, err := rand.Read(bs)
if err != nil {
return "", err
}
symbolsByteLength := byte(len(NonceSymbols))
for i, b := range bs {
bs[i] = NonceSymbols[b%symbolsByteLength]
}
return string(bs), nil
}
// BuildAuthorization 构建请求头中的 Authorization 信息
func BuildAuthorization(
mchid string,
certificateSerialNo string,
privateKey *rsa.PrivateKey,
method string,
canonicalURL string,
body []byte,
) (string, error) {
const (
SignatureMessageFormat = "%s\n%s\n%d\n%s\n%s\n" // 数字签名原文格式
// HeaderAuthorizationFormat 请求头中的 Authorization 拼接格式
HeaderAuthorizationFormat = "WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",timestamp=\"%d\",serial_no=\"%s\",signature=\"%s\""
)
nonce, err := GenerateNonce()
if err != nil {
return "", err
}
timestamp := time.Now().Unix()
message := fmt.Sprintf(SignatureMessageFormat, method, canonicalURL, timestamp, nonce, body)
signature, err := SignSHA256WithRSA(message, privateKey)
if err != nil {
return "", err
}
authorization := fmt.Sprintf(
HeaderAuthorizationFormat,
mchid, nonce, timestamp, certificateSerialNo, signature,
)
return authorization, nil
}
// ExtractResponseBody 提取应答报文的 Body
func ExtractResponseBody(response *http.Response) ([]byte, error) {
if response.Body == nil {
return nil, nil
}
body, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("read response HttpBody err:[%s]", err.Error())
}
response.Body = io.NopCloser(bytes.NewBuffer(body))
return body, nil
}
const (
WechatPayTimestamp = "Wechatpay-Timestamp" // 微信支付回包时间戳
WechatPayNonce = "Wechatpay-Nonce" // 微信支付回包随机字符串
WechatPaySignature = "Wechatpay-Signature" // 微信支付回包签名信息
WechatPaySerial = "Wechatpay-Serial" // 微信支付回包平台序列号
RequestID = "Request-Id" // 微信支付回包请求ID
)
// ValidateResponse 验证微信支付回包的签名信息
func ValidateResponse(
wechatpayPublicKeyId string,
wechatpayPublicKey *rsa.PublicKey,
headers *http.Header,
body []byte,
) error {
requestID := headers.Get(RequestID)
timestampStr := headers.Get(WechatPayTimestamp)
serialNo := headers.Get(WechatPaySerial)
signature := headers.Get(WechatPaySignature)
nonce := headers.Get(WechatPayNonce)
// 拒绝过期请求
timestamp, err := strconv.ParseInt(timestampStr, 10, 64)
if err != nil {
return fmt.Errorf("invalid timestamp: %v", err)
}
if time.Now().Sub(time.Unix(timestamp, 0)) > 5*time.Minute {
return errors.New("invalid timestamp")
}
if serialNo != wechatpayPublicKeyId {
return fmt.Errorf(
"serial-no mismatch: got %s, expected %s, request-id: %s",
serialNo,
wechatpayPublicKeyId,
requestID,
)
}
message := fmt.Sprintf("%s\n%s\n%s\n", timestampStr, nonce, body)
if err := VerifySHA256WithRSA(message, signature, wechatpayPublicKey); err != nil {
return fmt.Errorf("invalid signature: %v, request-id: %s", err, requestID)
}
return nil
}
// ApiException 微信支付API错误异常发送HTTP请求成功但返回状态码不是 2XX 时抛出本异常
type ApiException struct {
HttpStatusCode int // 应答报文的 HTTP 状态码
HttpHeader http.Header // 应答报文的 Header 信息
HttpBody []byte // 应答报文的 Body 原文
ErrCode string // 微信支付回包的错误码
ErrMessage string // 微信支付回包的错误信息
}
func (c *ApiException) Error() string {
buf := bytes.NewBuffer(nil)
buf.WriteString(fmt.Sprintf("srv error:[StatusCode: %d, Body: %s", c.HttpStatusCode, string(c.HttpBody)))
if len(c.HttpHeader) > 0 {
buf.WriteString(" Header: ")
for key, value := range c.HttpHeader {
buf.WriteString(fmt.Sprintf("\n - %v=%v", key, value))
}
buf.WriteString("\n")
}
buf.WriteString("]")
return buf.String()
}
func (c *ApiException) StatusCode() int {
return c.HttpStatusCode
}
func (c *ApiException) Header() http.Header {
return c.HttpHeader
}
func (c *ApiException) Body() []byte {
return c.HttpBody
}
func (c *ApiException) ErrorCode() string {
return c.ErrCode
}
func (c *ApiException) ErrorMessage() string {
return c.ErrMessage
}
func NewApiException(statusCode int, header http.Header, body []byte) error {
ret := &ApiException{
HttpStatusCode: statusCode,
HttpHeader: header,
HttpBody: body,
}
bodyObject := map[string]interface{}{}
if err := json.Unmarshal(body, &bodyObject); err == nil {
if val, ok := bodyObject["code"]; ok {
ret.ErrCode = val.(string)
}
if val, ok := bodyObject["message"]; ok {
ret.ErrMessage = val.(string)
}
}
return ret
}
// Time 复制 time.Time 对象,并返回复制体的指针
func Time(t time.Time) *time.Time {
return &t
}
// String 复制 string 对象,并返回复制体的指针
func String(s string) *string {
return &s
}
// Bool 复制 bool 对象,并返回复制体的指针
func Bool(b bool) *bool {
return &b
}
// Float64 复制 float64 对象,并返回复制体的指针
func Float64(f float64) *float64 {
return &f
}
// Float32 复制 float32 对象,并返回复制体的指针
func Float32(f float32) *float32 {
return &f
}
// Int64 复制 int64 对象,并返回复制体的指针
func Int64(i int64) *int64 {
return &i
}
// Int32 复制 int64 对象,并返回复制体的指针
func Int32(i int32) *int32 {
return &i
}
func (srv *MchConfig) Request(host, method, path string, reqBody []byte) (response []byte, err error) {
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
fmt.Print(reqUrl.Path)
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
if err != nil {
return nil, err
}
//httpRequest.Header.Set("mchid", srv.mchId)
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", srv.WechatPayPublicKeyId())
httpRequest.Header.Set("Content-Type", "application/json")
authorization, err := BuildAuthorization(
srv.MchId(),
srv.CertificateSerialNo(),
srv.PrivateKey(),
method,
reqUrl.Path,
reqBody,
)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = ValidateResponse(
srv.WechatPayPublicKeyId(),
srv.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return nil, err
}
return respBody, nil
}
return nil, &ApiException{
HttpStatusCode: httpResponse.StatusCode,
HttpHeader: httpResponse.Header,
HttpBody: respBody,
}
}
func (srv *MchConfig) Request2(host, method, path string, reqBody []byte) (response []byte, err error) {
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
if err != nil {
return nil, err
}
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
if err != nil {
return nil, err
}
httpRequest.Header.Set("Accept", "application/json")
httpRequest.Header.Set("Wechatpay-Serial", srv.WechatPayPublicKeyId())
httpRequest.Header.Set("Content-Type", "application/json")
httpRequest.Header.Set("mchid", "application/json")
authorization, err := BuildAuthorization(
srv.MchId(),
srv.CertificateSerialNo(),
srv.PrivateKey(),
method,
path,
reqBody,
)
if err != nil {
return nil, err
}
httpRequest.Header.Set("Authorization", authorization)
client := &http.Client{}
httpResponse, err := client.Do(httpRequest)
if err != nil {
return nil, err
}
respBody, err := ExtractResponseBody(httpResponse)
if err != nil {
return nil, err
}
//hs, _ := json.Marshal(httpRequest.Header)
//fmt.Printf("\npath=%s\nreqBody=%s\nheaders=%s\n", path, string(reqBody), string(hs))
//fmt.Printf("\nrespBody=%s\n", string(respBody))
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
// 2XX 成功,验证应答签名
err = ValidateResponse(
srv.WechatPayPublicKeyId(),
srv.WechatPayPublicKey(),
&httpResponse.Header,
respBody,
)
if err != nil {
return nil, err
}
return respBody, nil
}
return nil, &ApiException{
HttpStatusCode: httpResponse.StatusCode,
HttpHeader: httpResponse.Header,
HttpBody: respBody,
}
}
func (srv *MchConfig) Verify(request *http.Request) (string, error) {
respBody, err := io.ReadAll(request.Body)
if err != nil {
return "", fmt.Errorf("read request HttpBody err:[%s]", err.Error())
}
err = ValidateResponse(
srv.WechatPayPublicKeyId(),
srv.WechatPayPublicKey(),
&request.Header,
respBody,
)
if err != nil {
return "", err
}
return EncryptOAEPWithPublicKey(string(respBody), srv.wechatPayPublicKey)
}
func (srv *MchConfig) GetDecodeBody(headers *http.Header, respBody []byte) (*WxNotifyBody, string, error) {
if respBody == nil {
return nil, "", fmt.Errorf("request HttpBody is nil")
}
err := ValidateResponse(
srv.WechatPayPublicKeyId(),
srv.WechatPayPublicKey(),
headers,
respBody,
)
if err != nil {
return nil, "", err
}
var wxNotifyBody WxNotifyBody
if err = json.Unmarshal(respBody, &wxNotifyBody); err != nil {
return nil, "", err
}
aesUtil, err := NewAesUtil(srv.aesKey)
if err != nil {
return nil, "", err
}
decryptedText, err := aesUtil.DecryptToString(wxNotifyBody.Resource.AssociatedData, wxNotifyBody.Resource.Nonce, wxNotifyBody.Resource.Ciphertext)
if err != nil {
return nil, "", err
}
return &wxNotifyBody, decryptedText, nil
}
// BuildSortedQueryString 函数接受一个 map返回按照字段名排序后的 URL 键值对格式字符串
func BuildSortedQueryString(params map[string]any) string {
// 创建一个字符串切片,用于保存所有的键名
var keys []string
for key := range params {
keys = append(keys, key)
}
// 对键名进行 ASCII 字典顺序排序
sort.Strings(keys)
// 构建一个 URL 键值对字符串
var queryStrings []string
for _, key := range keys {
// 拼接 key=value
queryStrings = append(queryStrings, fmt.Sprintf("%s=%v", key, params[key]))
}
// 使用 & 连接所有的 key=value 对
return strings.Join(queryStrings, "&")
}
func Sha1(data string) string {
// 创建一个 SHA-1 哈希对象
hash := sha1.New()
// 写入数据
hash.Write([]byte(data))
// 计算并获取加密后的结果
hashBytes := hash.Sum(nil)
// 将结果转换为十六进制字符串
hashString := hex.EncodeToString(hashBytes)
// 打印加密后的 SHA-1 值
return hashString
}