为了保证支付接口使用的安全,微信支付平台在支付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
}