为了保证支付接口使用的安全,微信支付平台在支付API中使用了一些用于接口安全调用的技术。在调用时接口需要使用商户私钥进行接口调用的签名,获取到微信支付平台的应答之后也需要对应答进行签名验证。微信的应答签名使用平台证书来进行签名验证,因此在调用支付接口前还需要实现平台证书的下载以及管理。另外微信支付在回调通知和平台证书下载接口中,对关键信息进行了AES-256-GCM加密,因此开发者还需要了解如何使用APIv3密钥进行数据解密。在调用具体接口之前需要了解这是逻辑,并实现接口调用的一些基础代码。

11.1基本规则

商户接入微信支付,调用API必须遵循以下规则:
1)微信支付API v3使用 JSON 作为消息体的数据交换格式。请求须设置HTTP头部:

  • Content-Type: application/json
  • Accept: application/json
    2)请求的唯一标识
    微信支付给每个接收到的请求分配了一个唯一标识。请求的唯一标识包含在应答的HTTP头Request-ID中。
    3)错误信息
    微信支付API v3使用HTTP状态码来表示请求处理的结果。
  • 处理成功的请求,如果有应答的消息体将返回200,若没有应答的消息体将返回204。
  • 已经被成功接受待处理的请求,将返回202。
  • 请求处理失败时,如缺少必要的入参、支付时余额不足,将会返回4xx范围内的错误码。
  • 请求处理时发生了微信支付侧的服务系统错误,将返回500/501/503的状态码。这种情况比较少见。
    4)User Agent
    HTTP协议要求发起请求的客户端在每一次请求中都使用HTTP头 User-Agent来标识自己。微信支付API v3很可能会拒绝处理无User-Agent 的请求。

11.2请求签名

微信支付使用APIv3密钥对请求进行签名。微信支付会在收到请求后进行签名的验证。如果签名验证不通过,微信支付将会拒绝处理请求,并返回401 Unauthorized。
开发人员调用支付接口时需要按照以下的规则构造签名串。签名串一共有五行,每一行为一个参数,行尾以 \n结束,包括最后一行。
HTTP请求方法\n
URL\n
请求时间戳\n
请求随机串\n
请求报文主体\n

然后使用商户私钥对待签名串进行SHA256 with RSA签名,并对签名结果进行Base64编码得到签名值。
微信支付要求请求使用HTTP Authorization头来传递签名。Authorization由认证类型和签名信息两个部分组成。具体内容为:

  • 认证类型,目前为WECHATPAY2-SHA256-RSA2048
  • 签名信息:包括发起请求的商户的商户号mchid,商户API证书的serial_no,请求随机串nonce_str,时间戳timestamp,签名值signature。
    Authorization 头的示例如下:
    Authorization:WECHATPAY2-SHA256-RSA2048 mchid=“1900009191”,nonce_str=“593BEC0C930BF1AFEB40B4A08C8FB242”,signature=“uOVRnA4qG…”,timestamp=“1554208460”,serial_no=“1DDE55AD98ED71D6EDD4A4A16996DE7B47773A8C”’
    下面我们一步步来实现向微信支付服务器发送一个POST请求,首先来看看如何生成向请求头中的Authorization信息。
    接下来首先给出商户数据结构的定义,定义商户对象是需要指定商户的类型(直连商户、服务商商户),以及商户的参数(商户号、商户关联的APPID), 以及为商户对象加载商户密钥以及商户证书。以下是商户结构的定义代码:
type MchWxapp struct {
   //商户类型 0直连商户 1服务商商户
   MchType       int
   //商户对应的appid
   Appid     string
   //商户号
   Mchid     string
   //商户的API v3密钥
   MchAPIKey  string
   //商户API私钥
   MchPrivateKey *rsa.PrivateKey
   //商户 API 证书
   MchCertificate *x509.Certificate
}

商户的API私钥用于生成调用签名,接下来给出商户密钥的加载代码:

func LoadPrivateKeyWithPath(path string) (privateKey *rsa.PrivateKey, err error) {
   privateKeyBytes, err := ioutil.ReadFile(path)
   if err != nil {
      return nil, err 
   }
   block, _ := pem.Decode([]byte(privateKeyStr))
   if block == nil {
      return nil, fmt.Errorf("decode private key err")
   }
   key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
   if err != nil {
      return nil, err 
   }
   privateKey, ok := key.(*rsa.PrivateKey)
   if !ok {
      return nil, fmt.Errorf("%s is not rsa private key", privateKeyStr)
   }
   return privateKey, nil
}

生成请求头Authorization信息时需要用到商户证书的SerialNumber,以下是商户证书的加载代码:

func LoadCertificateWithPath(path string) (certificate *x509.Certificate, err error) {
   certificateBytes, err := ioutil.ReadFile(path)
   if err != nil {
      return nil, err 
   }
   block, _ := pem.Decode([]byte(certificateStr))
   if block == nil {
      return nil, fmt.Errorf("decode certificate err")
   }
   certificate, err = x509.ParseCertificate(block.Bytes)
   if err != nil {
      return nil, err 
   }
}

接下来要进行签名串的构造以及对签名串进行签名,具体代码如下:

func GenerateWxPayReqHeader(ctx *MchParam, method string, rawUrl string, signBody string) (authorization string, err error){
   timestamp := time.Now().Unix()
   url, err := url.Parse(rawUrl)
   if err != nil {
      return "", err
   }
   nonce, err := GenerateNonce()
   if err != nil {
      return "", err
   }

   SignatureMessageFormat := "%s\n%s\n%d\n%s\n%s\n"
   message := fmt.Sprintf(SignatureMessageFormat, method, url.RequestURI(), timestamp, nonce, signBody)
   signatureResult, err := SignSHA256WithRSA(ctx.MchPrivateKey, message)
   if err != nil {
      return "", err
   }

   certSerialNo := fmt.Sprintf("%X", ctx.MchCertificate.SerialNumber)
   HeaderAuthorizationFormat := "WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",timestamp=\"%d\",serial_no=\"%s\",signature=\"%s\""
   authorization = fmt.Sprintf(HeaderAuthorizationFormat, ctx.Mchid, nonce, timestamp, certSerialNo, signatureResult)
   return authorization, nil
}

代码中使用GenerateNonce()生成一个32个字节的请求随机串,并调用SignSHA256WithRSA对待签名串进行SHA256 with RSA签名。下面是函数SignSHA256WithRSA的实现:

func SignSHA256WithRSA(privateKey *rsa.PrivateKey, source string) 
	(signature string, err error) {
   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
}

最后来我们通过代码来看看如何通过HTTP的POST方法来调用支付接口。以下代码中去掉了响应数据签名验证的逻辑,响应数据签名验证稍后再来分析:

func WxPayPostV3(ctx *MchParam, url string, data []byte) (string, error) {
   token, err := GenerateWxPayReqHeader(ctx, http.MethodPost, url, string(data))
   if err != nil {
      log.Println(err)
      return "", err
   }

   request, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
   if err != nil {
      return "", err
   }
   request.Header.Add("Authorization", token)
   request.Header.Add("User-Agent", "go pay sdk")
   request.Header.Add("Content-type", "application/json;charset='utf-8'")
   request.Header.Add("Accept", "application/json")

   client := &http.Client{Timeout: 5 * time.Second}
   resp, err := client.Do(request)
   if err != nil {
      log.Println(err)
      return "", err
   }
   defer resp.Body.Close()

   result, _ := ioutil.ReadAll(resp.Body)
   if resp.StatusCode != 200 && resp.StatusCode != 204 {
      err := fmt.Errorf("status:%d;msg=%s", resp.StatusCode, string(result))
      log.Println(err)
      return string(result), err
   }

   return string(result), nil
}