Login expired. Please log in again.

Feedback

0/300

Feedback

Submitted successfully

ok

Feedback

Network exception, please try again later

ok

签名生成

商户可以按照下述步骤生成请求的签名。在本节的最后,我们准备了多种常用编程语言的演示代码供开发者参考。

微信支付API V3 要求商户对请求进行签名。微信支付会在收到请求后进行签名的验证。如果签名验证不通过,微信支付API V3将会拒绝处理请求,并返回401 Unauthorized。

1. 准备

商户需要拥有一个微信支付商户号,并通过超级管理员账号登录商户平台,获取商户API证书。 商户API证书的压缩包中包含了签名必需的私钥和商户证书。

2. 构造签名串

我们希望商户的技术开发人员按照当前文档约定的规则构造签名串。微信支付会使用同样的方式构造签名串。如果商户构造签名串的方式错误,将导致签名验证不通过。下面先说明签名串的具体格式。

签名串一共有五行,每一行为一个参数。行尾以 \n(换行符,ASCII编码值为0x0A)结束,包括最后一行。如果参数本身以\n结束,也需要附加一个\n。

HTTP请求方法\ n
URL\ n
请求时间戳\ n
请求随机串\ n
请求报文主体\ n

我们通过在命令行中调用"获取微信支付平台证书"接口,一步一步向开发者介绍如何进行请求签名。按照接口文档,获取商户平台证书的URL为 https://apihk.mch.weixin.qq.com/v3/global/certificates请求方法为GET,没有查询参数。

第一步,获取HTTP请求的方法(GET,POST,PUT)等

GET

第二步,获取请求的绝对URL,并去除域名部分得到参与签名的URL。如果请求中有查询参数,URL末尾应附加有'?'和对应的查询字符串。

/v3/global/certificates

第三步,获取发起请求时的系统当前时间戳,即格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数,作为请求时间戳。微信支付会拒绝处理很久之前发起的请求,请商户保持自身系统的时间准确。

 $ date +%s 
 1554208460

第四步,生成一个请求随机串,可参见生成随机数算法。这里,我们使用命令行直接生成一个。

$ hexdump -n 16 -e '4/4 "%08X" 1 "\n"' /dev/random 
 593BEC0C930BF1AFEB40B4A08C8FB242

第五步,获取请求中的请求报文主体(request body)。

• 请求方法为GET时,报文主体为空。

• 当请求方法为POST或PUT时,请使用真实发送的JSON报文。

• 图片上传API,请使用meta对应的JSON报文。

对于下载证书的接口来说,请求报文主体是一个空串。

第六步,按照前述规则,构造的请求签名串为:

GET\n 
 /v3/global/certificates\n 
 1554208460\n 
 593BEC0C930BF1AFEB40B4A08C8FB242\n 
 \n

3. 计算签名值

绝大多数编程语言提供的签名函数支持对签名数据进行签名。强烈建议商户调用该类函数,使用商户私钥对待签名串进行SHA256 with RSA签名,并对签名结果进行Base64编码得到签名值。

$ echo -n -e \
 "GET\n/v3/global/certificates\n1554208460\n593BEC0C930BF1AFEB40B4A08C8FB242\n\n" \
 | openssl dgst -sha256 -sign apiclient_key.pem \
 | openssl base64 -A
 uOVRnA4qG/MNnYzdQxJanN+zU+lTgIcnU9BxGw5dKjK+VdEUz2FeIoC+D5sB/LN+nGzX3hfZg6r5wT1pl2ZobmIc6p0ldN7J6yDgUzbX8Uk3sD4a4eZVPTBvqNDoUqcYMlZ9uuDdCvNv4TM3c1WzsXUrExwVkI1XO5jCNbgDJ25nkT/c1gIFvqoogl7MdSFGc4W4xZsqCItnqbypR3RuGIlR9h9vlRsy7zJR9PBI83X8alLDIfR1ukt1P7tMnmogZ0cuDY8cZsd8ZlCgLadmvej58SLsIkVxFJ8XyUgx9FmutKSYTmYtWBZ0+tNvfGmbXU7cob8H/4nLBiCwIUFluw==

4. 设置HTTP头

微信支付商户API V3要求请求通过HTTP Authorization头来传递签名。 Authorization由认证类型和签名信息两个部分组成。

下面我们使用命令行演示如何生成签名。

Authorization: 认证类型 签名信息

具体组成为:

1.认证类型,目前为WECHATPAY2-SHA256-RSA2048

2.签名信息

• 发起请求的商户(包括直连商户、服务商或渠道商)的商户号 mchid

• 商户API证书序列号serial_no,用于声明所使用的证书

• 请求随机串nonce_str

• 时间戳timestamp

• 签名值signature

注意
以上五项签名信息,无顺序要求。

Authorization 头的示例如下:(注意,示例因为排版可能存在换行,实际数据应在一行)

Authorization: WECHATPAY2-SHA256-RSA2048 mchid="1900009191",nonce_str="593BEC0C930BF1AFEB40B4A08C8FB242",signature="uOVRnA4qG/MNnYzdQxJanN+zU+lTgIcnU9BxGw5dKjK+VdEUz2FeIoC+D5sB/LN+nGzX3hfZg6r5wT1pl2ZobmIc6p0ldN7J6yDgUzbX8Uk3sD4a4eZVPTBvqNDoUqcYMlZ9uuDdCvNv4TM3c1WzsXUrExwVkI1XO5jCNbgDJ25nkT/c1gIFvqoogl7MdSFGc4W4xZsqCItnqbypR3RuGIlR9h9vlRsy7zJR9PBI83X8alLDIfR1ukt1P7tMnmogZ0cuDY8cZsd8ZlCgLadmvej58SLsIkVxFJ8XyUgx9FmutKSYTmYtWBZ0+tNvfGmbXU7cob8H/4nLBiCwIUFluw==",timestamp="1554208460",serial_no="1DDE55AD98ED71D6EDD4A4A16996DE7B47773A8C"

最终我们可以组一个包含了签名的HTTP请求了。

$ curl https://apihk.mch.weixin.qq.com/v3/global/certificates -H 'Authorization: WECHATPAY2-SHA256-RSA2048 mchid="1900009191",nonce_str="593BEC0C930BF1AFEB40B4A08C8FB242",signature="uOVRnA4qG/MNnYzdQxJanN+zU+lTgIcnU9BxGw5dKjK+VdEUz2FeIoC+D5sB/LN+nGzX3hfZg6r5wT1pl2ZobmIc6p0ldN7J6yDgUzbX8Uk3sD4a4eZVPTBvqNDoUqcYMlZ9uuDdCvNv4TM3c1WzsXUrExwVkI1XO5jCNbgDJ25nkT/c1gIFvqoogl7MdSFGc4W4xZsqCItnqbypR3RuGIlR9h9vlRsy7zJR9PBI83X8alLDIfR1ukt1P7tMnmogZ0cuDY8cZsd8ZlCgLadmvej58SLsIkVxFJ8XyUgx9FmutKSYTmYtWBZ0+tNvfGmbXU7cob8H/4nLBiCwIUFluw==",timestamp="1554208460",serial_no="1DDE55AD98ED71D6EDD4A4A16996DE7B47773A8C"'

5. 演示代码

开发者可以查看开发工具 相关章节,获取对应语言的库。如何在程序中加载私钥,请参考常见问题 。

计算签名的示例代码如下。

import okhttp3.HttpUrl;
package com.wechat.v3;

import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpRequestBase;
import org.junit.Test;

import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.util.Base64;

public class SignTest {
protected static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
protected static final SecureRandom RANDOM = new SecureRandom();
protected static final PrivateKey privateKey = null; //need to be initialized
protected static final String certificateSerialNumber = null; //need to be initialized
protected static final String merchantId = null; //need to be initialized

@Test
public void signTest() throws IOException {
HttpGet httpGet = new HttpGet("https://apihk.mch.weixin.qq.com/v3/global/certificates");
httpGet.addHeader("Accept", "application/json");
httpGet.addHeader("Content-type", "application/json; charset=utf-8");
System.out.println(getToken(httpGet,""));
}

public String getToken(HttpRequestBase request, String body) throws IOException {
String nonceStr = generateNonceStr();
long timestamp = generateTimestamp();

String message = buildMessage(nonceStr, timestamp, request, body);
// log.debug("authorization message=[{}]", message);
String signature = sign(message.getBytes(StandardCharsets.UTF_8));

String token = "mchid=\"" + merchantId + "\","
+ "nonce_str=\"" + nonceStr + "\","
+ "timestamp=\"" + timestamp + "\","
+ "serial_no=\"" + certificateSerialNumber + "\","
+ "signature=\"" + signature + "\"";
// log.debug("authorization token=[{}]", token);

return token;
}

public String sign(byte[] message) {
try {
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(privateKey);
sign.update(message);
return Base64.getEncoder().encodeToString(sign.sign());
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("当前Java环境不支持SHA256withRSA", e);
} catch (SignatureException e) {
throw new RuntimeException("签名计算失败", e);
} catch (InvalidKeyException e) {
throw new RuntimeException("无效的私钥", e);
}
}

protected String buildMessage(String nonce, long timestamp, HttpRequestBase request, String body) throws IOException {
URI uri = request.getURI();
String canonicalUrl = uri.getRawPath();
if (uri.getQuery() != null) {
canonicalUrl += "?" + uri.getRawQuery();
}

return request.getRequestLine().getMethod() + "\n"
+ canonicalUrl + "\n"
+ timestamp + "\n"
+ nonce + "\n"
+ body + "\n";
}

protected long generateTimestamp() {
return System.currentTimeMillis() / 1000;
}

protected String generateNonceStr() {
char[] nonceChars = new char[32];
for (int index = 0; index < nonceChars.length; ++index) {
nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
}
return new String(nonceChars);
}

}
<? php
require_once('vendor/autoload.php');

use WeChatPay\ Crypto\ Rsa;

class SignTest {
	public static
	function timestamp(): int {
		return time();
	}

	public static
	function nonce(int $size = 32): string {
		if ($size < 1) {
			throw new InvalidArgumentException('Size must be a positive integer.');
		}

		return implode('', array_map(static
			function(string $c): string {
				return '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' [ord($c) % 62];
			}, str_split(random_bytes($size))));
	}

	public static
	function request(string $method, string $uri, string $timestamp, string $nonce, string $body = ''): string {
		return static::joinedByLineFeed($method, $uri, $timestamp, $nonce, $body);
	}

	public static
	function joinedByLineFeed(...$pieces): string {
		return implode("\n", array_merge($pieces, ['']));
	}

	public static
	function sign(string $message, $privateKey): string {
		if (!openssl_sign($message, $signature, $privateKey, OPENSSL_ALGO_SHA256)) {
			throw new UnexpectedValueException('Signing the input $message failed, please checking your $privateKey whether or nor correct.');
		}

		return base64_encode($signature);
	}

	public static
	function authorization(string $mchid, $privateKey, string $serial, string $method, string $uri, string $body = ''): string {
		$nonce = static::nonce();
		$timestamp = static::timestamp();
		$signature = static::sign(static::request($method, $uri, $timestamp, $nonce, $body), $privateKey);
		return sprintf(
			'WECHATPAY2-SHA256-RSA2048 mchid="%s",serial_no="%s",timestamp="%s",nonce_str="%s",signature="%s"',
			$mchid, $serial, $timestamp, $nonce, $signature
		);
	}
}

$merchantPrivateKeyFilePath = '/path/to/your/private/key/file';
$merchantId = '10010000';
$method = 'GET';
$uri = 'v3/global/certificates';
$merchantCertificateSerial = '329E9A85CDAAFAD289AA69AE71369CBF8A1290A2';
$merchantPrivateKeyFileContent = file_get_contents($merchantPrivateKeyFilePath);
$merchantPrivateKey = Rsa::from($merchantPrivateKeyFileContent, Rsa::KEY_TYPE_PRIVATE);

echo SignTest::authorization('', $merchantPrivateKey, $merchantCertificateSerial, $method, $uri);
package sign

import (
	"crypto"
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/base64"
	"encoding/pem"
	"fmt"
	"github.com/wechatpay-apiv3/wechatpay-go/utils"
	"log"
	"testing"
	"time"
)

const (
	NonceSymbols              = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
	NonceLength               = 32
	SignatureMessageFormat    = "%s\n%s\n%d\n%s\n%s\n"
	HeaderAuthorizationFormat = "%s mchid=\"%s\",nonce_str=\"%s\",timestamp=\"%d\",serial_no=\"%s\",signature=\"%s\""
)

var (
	signTestMchid            = ""                               // merchant id
	signTestCertSerialNumber = "" // merchant certificate serial number
	signTestPrivateKey       *rsa.PrivateKey
)

func GenerateNonce() (string, error) {
	bytes := make([]byte, NonceLength)
	_, err := rand.Read(bytes)
	if err != nil {
		return "", err
	}
	symbolsByteLength := byte(len(NonceSymbols))
	for i, b := range bytes {
		bytes[i] = NonceSymbols[b%symbolsByteLength]
	}
	return string(bytes), 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("%s is not rsa private key", privateKeyStr)
	}
	return privateKey, nil
}

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
}

func getAuthorizationType() string {
	return "WECHATPAY2-SHA256-RSA2048"
}

func GenerateAuthorizationHeader(mchid, certificateSerialNo, method, canonicalURL, body string, privateKey *rsa.PrivateKey) (string, error) {
	nonce, err := GenerateNonce()
	if err != nil {
		return "", err
	}
	timestamp := time.Now().Unix()
	message := fmt.Sprintf(SignatureMessageFormat, method, canonicalURL, timestamp, nonce, body)
	signatureResult, err := SignSHA256WithRSA(message, privateKey)
	if err != nil {
		return "", err
	}
	authorization := fmt.Sprintf(
		HeaderAuthorizationFormat, getAuthorizationType(),
		mchid, nonce, timestamp, certificateSerialNo, signatureResult,
	)
	return authorization, nil
}

func TestGenerateAuthorizationHeader(t *testing.T) {
	// 使用 utils 提供的函数从本地文件中加载商户私钥,商户私钥会用来生成请求的签名
	signTestPrivateKey, err = utils.LoadPrivateKeyWithPath("/Path/to/your/private_key.pem")
	if err != nil {
		log.Fatal("load merchant private key error")
	}
	authHeader, err := GenerateAuthorizationHeader(signTestMchid, signTestCertSerialNumber, "GET", "/v3/global/certificates", "", signTestPrivateKey)
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("The authorization header is: %s", authHeader)
}
说明
如果您的请求返回了签名错误401 Unauthorized,请参考 常见问题之签名相关
    页面导航

About  WeChat  Pay

Powered By Tencent & Tenpay Copyright©

2005-2024 Tenpay All Rights Reserved.

Contact Us
Wechat Pay Global

WeChat Pay Global

置顶