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

11.1基本规则

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

Content-Type: application/jsonAccept: 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

}