golang实现微信支付v3版本

Posted 快乐源泉

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了golang实现微信支付v3版本相关的知识,希望对你有一定的参考价值。

一、准备阶段

获取私钥

官方文档 https://kf.qq.com/faq/161222N...
获取私钥证书的序列号 https://pay.weixin.qq.com/wik...

openssl x509 -in 1900009191_20180326_cert.pem -noout -serial
serial=1DDE55AD98ED71D6EDD4A4A16996DE7B47773A8C

私钥获取后有三个文件

apiclient_key.p12 
apiclient_cert.pem  
apiclient_key.pem

本次示例程序中,使用的是文件 apiclient_key.pem内容

获取公钥(平台证书)

官方文档
更新证书 https://pay.weixin.qq.com/wik...
平台证书会提前10天生成新证书,微信官方推荐在旧证书过期前5-10天部署新证书
获取证书API文档 https://pay.weixin.qq.com/wik...
身份证认证信息生成文档 https://pay.weixin.qq.com/wik...

常量

const appId = ""           // 小程序或者公众号的appid
const mchId = ""           // 微信支付的商户id
const privateSerialNo = "" // 私钥证书号
const aesKey = ""          // 微信支付aes key\'

需要用的包


import (
    "crypto"
    "crypto/aes"
    "crypto/cipher"
    cryptoRand "crypto/rand"
    "crypto/rsa"
    "crypto/sha256"
    "crypto/x509"
    "encoding/base64"
    "encoding/pem"
    "errors"
    "fmt"
    "math/rand"
    "time"
)

import (
    "bytes"
    "crypto"
    "encoding/json"
    "errors"
    "fmt"
    "io/ioutil"
    "net/http"
    "net/url"
    "os"
    "sync"
    "time"
)

生成数字签名

// 对消息的散列值进行数字签名
func signPKCS1v15(msg, privateKey []byte, hashType crypto.Hash) ([]byte, error) {
    block, _ := pem.Decode(privateKey)
    if block == nil {
        return nil, errors.New("private key decode error")
    }
    pri, err := x509.ParsePKCS8PrivateKey(block.Bytes)
    if err != nil {
        return nil, errors.New("parse private key error")
    }
    key, ok := pri.(*rsa.PrivateKey)
    if ok == false {
        return nil, errors.New("private key format error")
    }
    sign, err := rsa.SignPKCS1v15(cryptoRand.Reader, key, hashType, msg)
    if err != nil {
        return nil, errors.New("sign error")
    }
    return sign, nil
}
// base编码
func base64EncodeStr(src []byte) string {
    return base64.StdEncoding.EncodeToString(src)
}

生成身份认证信息

func authorization(method string, paramMap map[string]interface{}, rawUrl string) (token string, err error) {
    var body string
    if len(paramMap) != 0 {
        paramJsonBytes, err := json.Marshal(paramMap)
        if err != nil {
            return token, err
        }
        body = string(paramJsonBytes)
    }
    urlPart, err := url.Parse(rawUrl)
    if err != nil {
        return token, err
    }
    canonicalUrl := urlPart.RequestURI()
    timestamp := time.Now().Unix()
    nonce := getRandomString(32)
    message := fmt.Sprintf("%s\\n%s\\n%d\\n%s\\n%s\\n", method, canonicalUrl, timestamp, nonce, body)
    open, err := os.Open("./private.pem") // 商户私有证书路径或者从数据库读取
    if err != nil {
        return token, err
    }
    defer open.Close()
    privateKey, err := ioutil.ReadAll(open)
    if err != nil {
        return token, err
    }
    signBytes, err := signPKCS1v15(hasha256(message), privateKey, crypto.SHA256)
    if err != nil {
        return token, err
    }
    sign := base64EncodeStr(signBytes)
    token = fmt.Sprintf("mchid=\\"%s\\",nonce_str=\\"%s\\",timestamp=\\"%d\\",serial_no=\\"%s\\",signature=\\"%s\\"",
        mchId, nonce, timestamp, privateSerialNo, sign)
    return token, nil
}

报文解密

func decryptGCM(aesKey, nonceV, ciphertextV, additionalDataV string) ([]byte, error) {
    key := []byte(aesKey)
    nonce := []byte(nonceV)
    additionalData := []byte(additionalDataV)
    ciphertext, err := base64.StdEncoding.DecodeString(ciphertextV)
    if err != nil {
        return nil, err
    }
    block, err := aes.NewCipher(key)
    if err != nil {
        return nil, err
    }
    aesGCM, err := cipher.NewGCM(block)
    if err != nil {
        return nil, err
    }
    plaintext, err := aesGCM.Open(nil, nonce, ciphertext, additionalData)
    if err != nil {
        return nil, err
    }
    return plaintext, err
}

获取平台证书

// 获取公钥
const publicKeyUrl = "https://api.mch.weixin.qq.com/v3/certificates"
type TokenResponse struct {
    Data []TokenResponseData `json:"data"`
}
type TokenResponseData struct {
    EffectiveTime      string             `json:"effective_time"`
    EncryptCertificate EncryptCertificate `json:"encrypt_certificate"`
    ExpireTime         string             `json:"expire_time"`
    SerialNo           string             `json:"serial_no"`
}
type EncryptCertificate struct {
    Algorithm      string `json:"algorithm"`
    AssociatedData string `json:"associated_data"`
    Ciphertext     string `json:"ciphertext"`
    Nonce          string `json:"nonce"`
}
var publicSyncMap sync.Map
// 获取公钥
func getPublicKey() (key string, err error) {
    var prepareTime int64 = 24 * 3600 * 3 // 证书提前三天过期旧证书,获取新证书
    nowTime := time.Now().Unix()
    // 读取公钥缓存数据
    cacheValueKey := fmt.Sprintf("app_id:%s:public_key:value", appId)
    cacheExpireTimeKey := fmt.Sprintf("app_id:%s:public_key:expire_time", appId)
    cacheValue, keyValueOk := publicSyncMap.Load(cacheValueKey)
    cacheExpireTime, expireTimeOk := publicSyncMap.Load(cacheExpireTimeKey)
    if keyValueOk && expireTimeOk {
        // 格式化时间
        local, _ := time.LoadLocation("Local")
        location, _ := time.ParseInLocation(time.RFC3339, cacheExpireTime.(string), local)
        // 判断是否过期,证书没有过期直接返回
        if location.Unix()-prepareTime > nowTime {
            return cacheValue.(string), nil
        }
    }
    token, err := authorization(http.MethodGet, nil, publicKeyUrl)
    if err != nil {
        return key, err
    }
    request, err := http.NewRequest(http.MethodGet, publicKeyUrl, nil)
    if err != nil {
        return key, err
    }
    request.Header.Add("Authorization", "WECHATPAY2-SHA256-RSA2048 "+token)
    request.Header.Add("User-Agent", "用户代理(https://zh.wikipedia.org/wiki/User_agent)")
    request.Header.Add("Content-type", "application/json;charset=\'utf-8\'")
    request.Header.Add("Accept", "application/json")
    client := http.DefaultClient
    response, err := client.Do(request)
    if err != nil {
        return key, err
    }
    defer response.Body.Close()
    bodyBytes, err := ioutil.ReadAll(response.Body)
    if err != nil {
        return key, err
    }
    //fmt.Println(string(bodyBytes))
    var tokenResponse TokenResponse
    if err = json.Unmarshal(bodyBytes, &tokenResponse); err != nil {
        return key, err
    }
    for _, encryptCertificate := range tokenResponse.Data {
        // 格式化时间
        local, _ := time.LoadLocation("Local")
        location, err := time.ParseInLocation(time.RFC3339, encryptCertificate.ExpireTime, local)
        if err != nil {
            return key, err
        }
        // 判断是否过期,证书没有过期直接返回
        if location.Unix()-prepareTime > nowTime {
            decryptBytes, err := decryptGCM(aesKey, encryptCertificate.EncryptCertificate.Nonce, encryptCertificate.EncryptCertificate.Ciphertext,
                encryptCertificate.EncryptCertificate.AssociatedData)
            if err != nil {
                return key, err
            }
            key = string(decryptBytes)
            publicSyncMap.Store(cacheValueKey, key)
            publicSyncMap.Store(cacheExpireTimeKey, encryptCertificate.ExpireTime)
            return key, nil
        }
    }
    return key, errors.New("get public key error")
}

二、发起微信支付

jsapi 发起支付

调用统一下单接口

统一下单接口文档 https://pay.weixin.qq.com/wik...

const commonPayUrl = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi"

// 统一下单接口
func commonPay() (payResMap map[string]string, err error) {
    payResMap = make(map[string]string)
    amount := 10
    paramMap := make(map[string]interface{})
    paramMap["appid"] = appId
    paramMap["mchid"] = mchId
    paramMap["description"] = fmt.Sprintf("微信充值:¥%d", amount)
    paramMap["out_trade_no"] = fmt.Sprintf("test%s%s", time.Now().Format("20060102150405"), randNumber())
    paramMap["notify_url"] = "http://tools.localhost/notify"
    paramMap["amount"] = map[string]interface{}{"total": amount * 100, "currency": "CNY"}
    paramMap["payer"] = map[string]string{"openid": "123456789"}
    token, err := authorization(http.MethodPost, paramMap, commonPayUrl)
    if err != nil {
        return payResMap, err
    }
    marshal, _ := json.Marshal(paramMap)
    request, err := http.NewRequest(http.MethodPost, commonPayUrl, bytes.NewReader(marshal))
    if err != nil {
        return payResMap, err
    }
    request.Header.Add("Authorization", "WECHATPAY2-SHA256-RSA2048 "+token)
    request.Header.Add("User-Agent", "用户代理(https://zh.wikipedia.org/wiki/User_agent)")
    request.Header.Add("Content-type", "application/json;charset=\'utf-8\'")
    request.Header.Add("Accept", "application/json")
    client := http.DefaultClient
    response, err := client.Do(request)
    if err != nil {
        return payResMap, err
    }
    defer func() {
        response.Body.Close()
    }()
    bodyBytes, err := ioutil.ReadAll(response.Body)
    if err != nil {
        return payResMap, err
    }
    if err = json.Unmarshal(bodyBytes, &payResMap); err != nil {
        return payResMap, err
    }
    if payResMap["prepay_id"] == "" {
        return payResMap, errors.New("code:" + payResMap["code"] + "err:" + payResMap["message"])
    }
    return payResMap, nil
}

生成jsapi发起支付

JSAPI 调起支付接口文档 https://pay.weixin.qq.com/wik...

func jsApi(payResMap map[string]string) (payJson string, err error) {
    payMap := make(map[string]string)
    timeStamp := time.Now().Unix()
    nonce := getRandomString(32)
    packageStr := "prepay_id=" + payResMap["prepay_id"]
    payMap["appId"] = appId
    payMap["timeStamp"] = fmt.Sprintf("%v", timeStamp)
    payMap["nonceStr"] = nonce
    payMap["package"] = packageStr
    // 签名
    message := fmt.Sprintf("%s\\n%s\\n%s\\n%s\\n", appId, fmt.Sprintf("%v", timeStamp), nonce, packageStr)
    open, err := os.Open("/Users/apple/data/www/go/work/src/study/testwechantpay/private.pem")
    if err != nil {
        return payJson, err
    }
    defer open.Close()
    privateKey, err := ioutil.ReadAll(open)
    if err != nil {
        return payJson, err
    }
    signBytes, err := signPKCS1v15(hasha256(message), privateKey, crypto.SHA256)
    if err != nil {
        return payJson, err
    }
    sign := base64EncodeStr(signBytes)
    payMap["signType"] = sign
    payMap["paySign"] = "RSA"
    payJsonBytes, err := json.Marshal(payMap)
    if err != nil {
        return payJson, err
    }
    payJson = string(payJsonBytes)
    return payJson, nil
}

前台发起支付js

需要加载微信js http://res.wx.qq.com/open/js/jweixin-1.6.0.js
调用微信js需要在微信支付平台,设置支付目录
指引文档 https://pay.weixin.qq.com/wik...

<script type="text/javascript" src="__STATIC__/frontend/js/jquery.min.js"></script>
<script type="text/javascript" src="http://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
<script>  
    $(function () {
        $(".am-btn").click(function () {
            var score = $(".score div input:checked").val();
            $.post("发起微信支付后端接口URL", {"score": score}, function (res) {
                if (res.status === 500) {
                    alert(res.message);
                    return;
                }
                if (typeof WeixinJSBridge == "undefined") {
                    if (document.addEventListener) {
                        document.addEventListener(\'WeixinJSBridgeReady\', onBridgeReady, false);
                    } else if (document.attachEvent) {
                        document.attachEvent(\'WeixinJSBridgeReady\', onBridgeReady);
                        document.attachEvent(\'onWeixinJSBridgeReady\', onBridgeReady);
                    }
                } else {
                    onBridgeReady(res);
                }
            })
        })
        function onBridgeReady(param) {
            var orderId = param.data.orderId;
            WeixinJSBridge.invoke(\'getBrandWCPayRequest\', {
                    "appId": param.data.appId,
                    "timeStamp": param.data.timeStamp,
                    "nonceStr": param.data.nonceStr,
                    "package": param.data.package,
                    "signType": param.data.signType,
                    "paySign": param.data.paySign
                },
                function (res) {
                    if (res.err_msg === "get_brand_wcpay_request:ok") {
                        window.location.href = "{:url(\'index/order/successful\')}?order_id=" + orderId;
                    }
                });
        }
    })
  </script>

三、异步通知

签名校验

文档 https://pay.weixin.qq.com/wik...
验证签名

//验证数字签名
func VerifyRsaSign(msg []byte, sign []byte, publicStr []byte, hashType crypto.Hash) bool {
    //pem解码
    block, _ := pem.Decode(publicStr)
    //x509解码
    publicKeyInterface, err := x509.ParseCertificate(block.Bytes)
    if err != nil {
        panic(err)
    }
    publicKey := publicKeyInterface.PublicKey.(*rsa.PublicKey)
    //验证数字签名
    err = rsa.VerifyPKCS1v15(publicKey, hashType, msg, sign) //crypto.SHA1
    return err == nil
}
// 验证签名
func signatureValidate(timeStamp, rawPost, nonce, signature string) (bool, error) {
    signature = base64DecodeStr(signature)
    message := fmt.Sprintf("%s\\n%s\\n%s\\n", timeStamp, nonce, rawPost)
    publicKey, err := getPublicKey()
    if err != nil {
        return false, err
    }

    return VerifyRsaSign(hasha256(message), []byte(signature), []byte(publicKey), crypto.SHA256), nil
}

报文解密

type NotifyResponse struct {
    CreateTime string         `json:"create_time"`
    Resource   NotifyResource `json:"resource"`
}
type NotifyResource struct {
    Ciphertext     string `json:"ciphertext"`
    AssociatedData string `json:"associated_data"`
    Nonce          string `json:"nonce"`
}
func notifyDecrypt(rawPost string) (decrypt string, err error) {
    var notifyResponse NotifyResponse
    if err = json.Unmarshal([]byte(rawPost), &notifyResponse); err != nil {
        return decrypt, err
    }
    decryptBytes, err := decryptGCM(aesKey, notifyResponse.Resource.Nonce, notifyResponse.Resource.Ciphertext,
        notifyResponse.Resource.AssociatedData)
    if err != nil {
        return decrypt, err
    }
    decrypt = string(decryptBytes)
    return decrypt, nil
}

四、查询订单

文档 https://pay.weixin.qq.com/wik...

查询订单

const searchTradeUrl = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/%s?mchid=%s"

// 查询交易
func searchTrade(orderId string) (trade string, err error) {
  rawUrl := fmt.Sprintf(searchTradeUrl, orderId, mchId)
  token, err := authorization(http.MethodGet, nil, rawUrl)
  if err != nil {
      return trade, err
  }
  request, err := http.NewRequest(http.MethodGet, rawUrl, nil)
  if err != nil {
      return trade, err
  }
  request.Header.Add("Authorization", "WECHATPAY2-SHA256-RSA2048 "+token)
  request.Header.Add("User-Agent", "用户代理(https://zh.wikipedia.org/wiki/User_agent)")
  request.Header.Add("Content-type", "application/json;charset=\'utf-8\'")
  request.Header.Add("Accept", "application/json")

  client := http.DefaultClient

  response, err := client.Do(request)
  if err != nil {
      return trade, err
  }
  defer response.Body.Close()

  bodyBytes, err := ioutil.ReadAll(response.Body)
  if err != nil {
      return trade, err
  }

  // 验证签名
  timestamp := response.Header.Get("Wechatpay-Timestamp")
  nonce := response.Header.Get("Wechatpay-Nonce")
  signature := response.Header.Get("Wechatpay-Signature")
  flag, err := signatureValidate(timestamp, string(bodyBytes), nonce, signature)
  if err != nil {
      return trade, err
  }
  if !flag {
      return trade, errors.New("search refund result verify sign error")
  }


  return string(bodyBytes), nil
}

五、申请退款

文档 https://pay.weixin.qq.com/wik...

申请退款

const refundUrl = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds"

func refundTrade(orderId string, amount float64) (trade string, err error) {
  paramMap := make(map[string]interface{})
  paramMap["out_trade_no"] = orderId
  paramMap["out_refund_no"] = orderId + "-1"
  paramMap["amount"] = map[string]interface{}{"refund": amount * 100, "total": amount * 100, "currency": "CNY"}

  token, err := authorization(http.MethodPost, paramMap, refundUrl)
  if err != nil {
      return trade, err
  }

  marshal, _ := json.Marshal(paramMap)
  request, err := http.NewRequest(http.MethodPost, refundUrl, bytes.NewReader(marshal))
  if err != nil {
      return trade, err
  }
  request.Header.Add("Authorization", "WECHATPAY2-SHA256-RSA2048 "+token)
  request.Header.Add("User-Agent", "用户代理(https://zh.wikipedia.org/wiki/User_agent)")
  request.Header.Add("Content-type", "application/json;charset=\'utf-8\'")
  request.Header.Add("Accept", "application/json")

  client := http.DefaultClient

  response, err := client.Do(request)
  if err != nil {
      return trade, err
  }
  defer func() {
      response.Body.Close()
  }()

  bodyBytes, err := ioutil.ReadAll(response.Body)
  if err != nil {
      return trade, err
  }

  // 验证签名
  timestamp := response.Header.Get("Wechatpay-Timestamp")
  nonce := response.Header.Get("Wechatpay-Nonce")
  signature := response.Header.Get("Wechatpay-Signature")
  flag, err := signatureValidate(timestamp, string(bodyBytes), nonce, signature)
  if err != nil {
      return trade, err
  }
  if !flag {
      return trade, errors.New("search refund result verify sign error")
  }

  return string(bodyBytes), nil
}

以上是关于golang实现微信支付v3版本的主要内容,如果未能解决你的问题,请参考以下文章

JAVA实现微信支付V3

PHP对接第三方支付渠道之微信支付v3版本

微信支付开发 认清微信支付v2和v3

Python3 微信支付(小程序支付)V3接口

Java中的微信支付:API V3 微信平台证书的获取与刷新

微信支付V3 小程序支付API Java版