开始使用
请求鉴权

请求鉴权

申请安全凭证

本文使用的安全凭证为密钥,密钥包括 SecretIdPrivateKey

  • SecretId(沙箱环境或生产环境):用于标识 API 调用者身份,是调用方的唯一标识。(在管理控制台-开发者参数页面查看)
  • PrivateKey(沙箱环境或生产环境):用于验证 API 调用者的身份,RSA私钥。(用命令生成公私钥对,私钥自行保管,公钥需在开发者参数页面填入:openssl genrsa -out private-key.pem 2048 && openssl rsa -in private-key.pem -pubout -out public-key.pem
  • PublicKey(沙箱环境或生产环境):收付通的公钥,调用方用于验证回调通知数据的合法性(在管理控制台-开发者参数页面查看)
  • 调用方必须严格保管安全凭证,避免泄露,否则将危及财产安全。如已泄漏,请立刻更换该安全凭证。

注:沙箱环境调试参考:沙箱环境调试

1.签名计算过程

下面以此示例详细介绍签名过程

https://p.wecard.tencent.com/cloudpay/v1/pay/query_order
 
Authorization: SHA256withRSA Credential=2nowiWOrZTloyCbZSLyu3vLkBVs/2024-10-23/wxpay/tc3_request, SignedHeaders=content-type;host;x-tc-ocode;x-tc-action, Signature=nFYlj2+vy5IPi3d/8yuDRTrhtlxRn71DUr/LohzUpKJIUq1z1BYbazCKreJjd0Ft/Zis/IyjQPEbvXLzt/OMtOvuNjboBeQ2woUfd/ZoQ8FtddP+gFKy+Nkl1rpJlYAy3oeLT2MRrPwuS9SJS4jzPKDSlhQ2/mby7DPxzIKF0wwEwFzmltsUY/2Rkv16Y8WMygiMTjzBXWdgCWrjlsNhA93arjdumANHsFg9D25M5yjwfbp3wjSb9UW507THTj12DkQwrGc7wsfqXRnlLraP3XUwd7SNJQdEQO+UEDidPoFILF1HRALGfxht+12sol7ifoslDZp2u4SGPeYhOoZnrQ==
Content-Type: application/json
Host: p.wecard.tencent.com
X-TC-Action: wxpay
X-TC-Version: 2023-04-13
X-TC-Timestamp: 1729683364
X-TC-Region: ap-shanghai
X-TC-Ocode: 123456
 
{"OutOrderId":"demo4343517950736398"}

1.1. 拼接规范请求串

按如下伪代码格式拼接规范请求串(CanonicalRequest):

CanonicalRequest =
    HTTPRequestMethod + '\n' +
    CanonicalURI + '\n' +
    CanonicalQueryString + '\n' +
    CanonicalHeaders + '\n' +
    SignedHeaders + '\n' +
    HashedRequestPayload
参数名称解释
HTTPRequestMethodHTTP 请求方法(GET、POST )。此示例取值为 POST
CanonicalURIURI 参数。此示例取值为 /cloudpay/v1/pay/query_order
CanonicalQueryString发起 HTTP 请求 URL 中的查询字符串,为 URL 中问号(?)后面的字符串内容,例如:orderId=xs1。
注意:CanonicalQueryString 需要参考 RFC3986 进行 URLEncode,字符集 UTF-8,推荐使用编程语言标准库进行编码。
CanonicalHeaders参与签名的头部信息;
1. 拼接规则:头部 key 和 value 统一转成小写,并去掉首尾空格,按照 key:value 格式拼接;2. 此示例计算结果是 content-type:application/json; charset=utf-8\nhost:p.wecard.tencent.com\nx-tc-ocode:123456\nx-tc-action:wxpay\n。
注意:content-type 必须和实际发送的相符合,有些编程语言网络库即使未指定也会自动添加 charset 值,如果签名时和发送时不一致,服务器会返回签名校验失败。
SignedHeaders参与签名的头部信息,说明此次请求有哪些头部参与了签名,和 CanonicalHeaders 包含的头部内容是一一对应的。content-type 和 host 为必选头部。
1. 拼接规则:头部 key 统一转成小写;
2. 此示例为 content-type;host;x-tc-ocode;x-tc-action
HashedRequestPayload请求正文(payload,即 body的哈希值,计算伪代码为 Lowercase(HexEncode(Hash.SHA256(RequestPayload))),即对 HTTP 请求正文做 SHA256 哈希,然后十六进制编码,最后编码串转换成小写字母。
对于 GET 请求,RequestPayload 固定为空字符串。此示例计算结果是 123836eabef01e14d334461d9037699deaeb8de329dd8e5c04069332a15dfc3a

根据以上规则,示例中得到的规范请求串如下:

POST
/cloudpay/v1/pay/query_order
 
content-type:application/json; charset=utf-8
host:p.wecard.tencent.com
x-tc-action:wxpay
x-tc-ocode: 123456
 
content-type;host;x-tc-ocode;x-tc-action
123836eabef01e14d334461d9037699deaeb8de329dd8e5c04069332a15dfc3a
 

1.2. 拼接待签名字符串

按如下格式拼接待签名字符串:

StringToSign =
    Algorithm + \n +
    RequestTimestamp + \n +
    CredentialScope + \n +
    HashedCanonicalRequest
参数名称解释
Algorithm签名算法,目前固定为TC3-HMAC-SHA256。
RequestTimestamp请求时间戳,即请求头部的公共参数 X-TC-Timestamp 取值,取当前时间 UNIX 时间戳,精确到秒。此示例取值为1681301329。
CredentialScope凭证范围,格式为 Date/service/tc3_request,包含日期、所请求的服务和终止字符串(tc3_request)。
Date 为 UTC 标准时间的日期,取值需要和公共参数 X-TC-Timestamp 换算的 UTC 标准时间日期一致;
service 为产品名,必须与调用的产品域名一致。
此示例计算结果是 2023-04-13/wxpay/tc3_request。
HashedCanonicalRequest前述步骤拼接所得规范请求串的哈希值,计算伪代码为 Lowercase(HexEncode(Hash.SHA256(CanonicalRequest)))。此示例计算结果是 fd99c930444e40b1e62b2846d198d2551537b4d77654403ee3f4e5db0cb725d9

根据以上规则,示例中得到的待签名字符串如下:

SHA256withRSA
1729683364
2024-10-23/wxpay/tc3_request
fd99c930444e40b1e62b2846d198d2551537b4d77654403ee3f4e5db0cb725d9

1.3.计算签名

1)计算签名,伪代码如下:

Signature = Base64Encode(SHA256withRSA(PrivateKey, StringToSign))

此示例计算结果是 nFYlj2+vy5IPi3d/8yuDRTrhtlxRn71DUr/LohzUpKJIUq1z1BYbazCKreJjd0Ft/Zis/IyjQPEbvXLzt/OMtOvuNjboBeQ2woUfd/ZoQ8FtddP+gFKy+Nkl1rpJlYAy3oeLT2MRrPwuS9SJS4jzPKDSlhQ2/mby7DPxzIKF0wwEwFzmltsUY/2Rkv16Y8WMygiMTjzBXWdgCWrjlsNhA93arjdumANHsFg9D25M5yjwfbp3wjSb9UW507THTj12DkQwrGc7wsfqXRnlLraP3XUwd7SNJQdEQO+UEDidPoFILF1HRALGfxht+12sol7ifoslDZp2u4SGPeYhOoZnrQ==。

1.4. 拼接Authorization

按如下格式拼接 Authorization:

Authorization =
    Algorithm + ' ' +
    'Credential=' + SecretId + '/' + CredentialScope + ', ' +
    'SignedHeaders=' + SignedHeaders + ', ' +
    'Signature=' + Signature
参数名称解释
Algorithm签名方法,固定为 TC3-HMAC-SHA256。
SecretId密钥对中的 SecretId,即 2nowiWOrZTloyCbZSLyu3vLkBVs。
CredentialScope见上文,凭证范围。此示例计算结果是2023-04-13/wxpay/tc3_request。
SignedHeaders见上文,参与签名的头部信息。此示例取值为 content-type;host;x-tc-ocode;x-tc-action。
Signature签名值。此示例计算结果是 nFYlj2+vy5IPi3d/8yuDRTrhtlxRn71DUr/LohzUpKJIUq1z1BYbazCKreJjd0Ft/Zis/IyjQPEbvXLzt/OMtOvuNjboBeQ2woUfd/ZoQ8FtddP+gFKy+Nkl1rpJlYAy3oeLT2MRrPwuS9SJS4jzPKDSlhQ2/mby7DPxzIKF0wwEwFzmltsUY/2Rkv16Y8WMygiMTjzBXWdgCWrjlsNhA93arjdumANHsFg9D25M5yjwfbp3wjSb9UW507THTj12DkQwrGc7wsfqXRnlLraP3XUwd7SNJQdEQO+UEDidPoFILF1HRALGfxht+12sol7ifoslDZp2u4SGPeYhOoZnrQ==。

根据以上规则,示例中得到的值为:

SHA256withRSA Credential=2nowiWOrZTloyCbZSLyu3vLkBVs/2024-10-23/wxpay/tc3_request, SignedHeaders=content-type;host;x-tc-ocode;x-tc-action, Signature=nFYlj2+vy5IPi3d/8yuDRTrhtlxRn71DUr/LohzUpKJIUq1z1BYbazCKreJjd0Ft/Zis/IyjQPEbvXLzt/OMtOvuNjboBeQ2woUfd/ZoQ8FtddP+gFKy+Nkl1rpJlYAy3oeLT2MRrPwuS9SJS4jzPKDSlhQ2/mby7DPxzIKF0wwEwFzmltsUY/2Rkv16Y8WMygiMTjzBXWdgCWrjlsNhA93arjdumANHsFg9D25M5yjwfbp3wjSb9UW507THTj12DkQwrGc7wsfqXRnlLraP3XUwd7SNJQdEQO+UEDidPoFILF1HRALGfxht+12sol7ifoslDZp2u4SGPeYhOoZnrQ==

最终完整的调用信息如下:

POST /cloudpay/v1/pay/query_order HTTP/1.1
Host: p.wecard.tencent.com
Authorization: SHA256withRSA Credential=2nowiWOrZTloyCbZSLyu3vLkBVs/2024-10-23/wxpay/tc3_request, SignedHeaders=content-type;host;x-tc-ocode;x-tc-action, Signature=nFYlj2+vy5IPi3d/8yuDRTrhtlxRn71DUr/LohzUpKJIUq1z1BYbazCKreJjd0Ft/Zis/IyjQPEbvXLzt/OMtOvuNjboBeQ2woUfd/ZoQ8FtddP+gFKy+Nkl1rpJlYAy3oeLT2MRrPwuS9SJS4jzPKDSlhQ2/mby7DPxzIKF0wwEwFzmltsUY/2Rkv16Y8WMygiMTjzBXWdgCWrjlsNhA93arjdumANHsFg9D25M5yjwfbp3wjSb9UW507THTj12DkQwrGc7wsfqXRnlLraP3XUwd7SNJQdEQO+UEDidPoFILF1HRALGfxht+12sol7ifoslDZp2u4SGPeYhOoZnrQ==
Content-Type: application/json; charset=utf-8
X-Tc-Action: wxpay
X-Tc-Ocode: 123456
X-Tc-Region: ap-shanghai
X-Tc-Timestamp: 1729683364
X-Tc-Version: 2023-04-13
 
{"OutOrderId":"demo4343517950736398"}

2. 回调请求验签

对于需要接收微卡收付通回调请求的业务,可以针对请求数据进行验签,判断回调的合法性;
验签的签名算法与业务系统请求微卡收付通的算法一致,只需修改验签逻辑中的加签元素即可。 需要替换的数据元素获取方式:

  • host: 替换为业务方回调地址的域名
  • uri: 替换为业务方回调地址中的 uri
  • timestamp: 替换为从回调请求 header 中获取的 X-TC-Timestamp
  • payload: 替换为从回调请求中获取的请求内容 body

将通过上述验签流程获取的数据进行校验,具体流程可以参考以下的代码演示。

3. 代码演示

JAVA

 
package org.example;
 
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.text.SimpleDateFormat;
import java.util.Base64;
import java.util.Date;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.bind.DatatypeConverter;
 
public class Sign {
    private final static String ocode = "";     // 客户的 appid,在管理控制台-开发者页面查看
    private final static String SECRET_ID = ""; // 客户的secretId,在管理控制台-开发者页面查看
    private final static String PRIVATE_KEY = ""; // 客户的 privateKey,private-key.pem 文件里面的内容。需提前在管理控制台-开发者页面设置好对应公钥
    private final static String PUBLIC_KEY = ""; // 微卡收付通的公钥,在管理控制台-开发者页面查看
    private final static String CT_JSON = "application/json; charset=utf-8";
    private final static String algorithm = "SHA256withRSA";
    private final static Charset UTF8 = StandardCharsets.UTF_8;
 
    private final static String region = "ap-shanghai";
    private final static String version = "2023-04-13";
 
    public static String sha256Hex(String s) throws NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        byte[] d = md.digest(s.getBytes(UTF8));
        return DatatypeConverter.printHexBinary(d).toLowerCase();
    }
 
    // genAuthorization 计算签名
    private static String genAuthorization(String host, String payload, String uri, String action, String timestamp)
            throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
        return genAuthorization(host, payload, uri, action, timestamp, false, "");
    }
 
    // genAuthorization 计算签名
    private static String genAuthorization(String host, String payload, String uri, String action, String timestamp,
                                           boolean isVerify, String signature)
            throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        // 注意时区,否则容易出错
        sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
        String date = sdf.format(new Date(Long.valueOf(timestamp + "000")));
 
 
        // ************* 步骤 1:拼接规范请求串 *************
        String httpRequestMethod = "POST";
        String service = "wxpay";
        String canonicalUri = uri;
        String canonicalQueryString = "";
        String canonicalHeaders = "content-type:application/json; charset=utf-8\n"
                + "host:" + host + "\n" + "x-tc-ocode:" + ocode + "\n" + "x-tc-action:" + action.toLowerCase() + "\n";
        String signedHeaders = "content-type;host;x-tc-ocode;x-tc-action";
 
        String hashedRequestPayload = sha256Hex(payload);
        String canonicalRequest = httpRequestMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n"
                + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestPayload;
        System.out.println(canonicalRequest); // 调试日志 todo
 
        // ************* 步骤 2:拼接待签名字符串 *************
        String credentialScope = date + "/" + service + "/" + "tc3_request";
        String hashedCanonicalRequest = sha256Hex(canonicalRequest);
        String stringToSign = algorithm + "\n" + timestamp + "\n" + credentialScope + "\n" + hashedCanonicalRequest;
        System.out.println(stringToSign); // 调试日志 todo
 
        if (isVerify) {
            // 如果是验签,这里走验签的逻辑
            return verify(stringToSign, signature) ? "1" : "0";
        }
 
        // ************* 步骤 3:计算签名 *************
        signature = sign(stringToSign);
        System.out.println(signature);  // 调试日志 todo
 
        // ************* 步骤 4:拼接 Authorization *************
        String authorization = algorithm + " " + "Credential=" + SECRET_ID + "/" + credentialScope + ", "
                + "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature;
        System.out.println(authorization); // 调试日志 todo
 
        return authorization;
    }
 
    private static boolean verify(String message, String signature) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
        Signature sign = Signature.getInstance(algorithm);
        sign.initVerify(loadPublicKeyFromString(PUBLIC_KEY));
        sign.update(message.getBytes(UTF8));
        return sign.verify(Base64.getDecoder().decode(signature));
    }
 
    private static String sign(String message) throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
        byte[] sign;
        Signature signature = Signature.getInstance(algorithm);
        signature.initSign(loadPrivateKeyFromString(PRIVATE_KEY));
        signature.update(message.getBytes(UTF8));
        sign = signature.sign();
        return Base64.getEncoder().encodeToString(sign);
    }
 
    /**
     * 从字符串中加载RSA私钥。
     *
     * @param keyString 私钥字符串
     * @return RSA私钥
     */
    public static PrivateKey loadPrivateKeyFromString(String keyString) {
        try {
            keyString =
                    keyString
                            .replace("-----BEGIN PRIVATE KEY-----", "")
                            .replace("-----END PRIVATE KEY-----", "")
                            .replaceAll("\\s+", "");
            return KeyFactory.getInstance("RSA")
                    .generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyString)));
        } catch (NoSuchAlgorithmException e) {
            throw new UnsupportedOperationException(e);
        } catch (InvalidKeySpecException e) {
            throw new IllegalArgumentException(e);
        }
    }
 
    /**
     * 从字符串中加载RSA公钥。
     *
     * @param keyString 公钥字符串
     * @return RSA公钥
     */
    public static PublicKey loadPublicKeyFromString(String keyString) {
        try {
            keyString =
                    keyString
                            .replace("-----BEGIN PUBLIC KEY-----", "")
                            .replace("-----END PUBLIC KEY-----", "")
                            .replaceAll("\\s+", "");
            return KeyFactory.getInstance("RSA")
                    .generatePublic(new X509EncodedKeySpec(Base64.getDecoder().decode(keyString)));
        } catch (NoSuchAlgorithmException e) {
            throw new UnsupportedOperationException(e);
        } catch (InvalidKeySpecException e) {
            throw new IllegalArgumentException(e);
        }
    }
 
    // 请求开放接口通用方法
    private static void doPost(String uri, String payload) {
        try {
            // 准备加签数据
            String host = "typ.wecard.tencent.com"; // 接口域名,如地址为 http://p.wecard.tencent.com/cloudpay/v1/pay/unified_order, 则 host 为 p.wecard.tencent.com
            String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
            String action = "wxpay";// 不用修改
            // 获取加签数据
            String authorization = genAuthorization(host, payload, uri, action, timestamp);
 
            // 组装请求数据
            HttpPost httpPost = new HttpPost("https://" + host + uri);
            httpPost.addHeader("Authorization", authorization);
            httpPost.addHeader("Content-Type", CT_JSON);
            httpPost.addHeader("Host", host);
            httpPost.addHeader("X-TC-Action", action);
            httpPost.addHeader("X-TC-Timestamp", timestamp);
            httpPost.addHeader("X-TC-Version", version);
            httpPost.addHeader("X-TC-Region", region);
            httpPost.addHeader("X-TC-Ocode", ocode);
 
            // 发起请求
            CloseableHttpClient httpClient = HttpClients.createDefault();
            httpPost.setEntity(new StringEntity(payload, "UTF-8"));
            CloseableHttpResponse response = httpClient.execute(httpPost);
            if (response.getStatusLine().getStatusCode() != 200) {
                System.err.println(response);
                throw new RuntimeException("HTTP request Exception");
            }
            String resultdata = EntityUtils.toString(response.getEntity(), "UTF-8");
            System.err.println(resultdata);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
 
    /*
     * checkCallBackSign 回调的验签流程
     * 回调时请求回调接口的加签逻辑,与请求开放接口的加签逻辑一致。只需要修改加签的参数即可
     * 1、准备主体的appid、secretId、secretKey
     * 2、从 http-header 获取 X-TC-Timestamp 计算签名的秒级时间戳(建议校验回调服务器的当前时间与该时间戳的时间差)
     * 3、获取回调接口的域名
     * 4、获取回调接口的uri
     * 5、获取回调的请求body
     * 6、将以上数据调用 genAuthorization 方法计算签名,与从 http-header 获取 Authorization 签名字符串比较是否相等
     *
     */
    private static void checkCallBackSign(HttpServletRequest request, HttpServletResponse response, String requestBody) throws Exception {
        String requestAuth = request.getHeader("Authorization");
        if (requestAuth == null || requestAuth.isEmpty()) {
            // 验签失败返回的状态码不能为 200
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }
 
        // 准备验签数据
        String host = ""; // 回调接口的域名domain,如回调地址为http://xxx.com/t/pay_notify, 则 domain 为 xxx.com
        String canonicalUri = ""; // 回调接口的路由uri,如回调地址为http://xxx.com/order/pay_notify, 则 uri 为 /order/pay_notify
        String timestamp = request.getHeader("X-TC-Timestamp");// 从header中获取X-TC-Timestamp加签使用的秒级的时间戳
        String action = "Notify"; // 不用修改
 
        System.out.println(host); // 调试日志 todo
        System.out.println(canonicalUri); // 调试日志 todo
        System.out.println(timestamp); // 调试日志 todo
        System.out.println(requestAuth); // 调试日志 todo
 
        // 计算加签
        String signature = extractSignature(requestAuth);
        String verifyResult = genAuthorization(host, requestBody, canonicalUri, action, timestamp, true, signature);
        boolean verified = "1".equals(verifyResult);
        System.out.println(verified); // 调试日志 todo
 
        // 判断签名是否正确
        if (!verified) {
            System.out.println("验签失败");
            // 验签失败返回的状态码不能为 200
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            return;
        }
 
        // todo 客户的业务逻辑
        System.out.println("验签成功");
 
        // 验签成功返回200 HTTP状态码
        response.setStatus(HttpServletResponse.SC_OK);
    }
 
    private static String extractSignature(String input) {
        String signaturePattern = "Signature=([^,]+)";
 
        Pattern pattern = Pattern.compile(signaturePattern);
        Matcher matcher = pattern.matcher(input);
 
        if (matcher.find()) {
            return matcher.group(1);
        }
        return "";
    }
 
    public static void main(String[] args) {
        // 请求查单接口示例
        String uri = "/cloudpay/v1/pay/query_order";
        doPost(uri, "{\"OutOrderId\":\"demo4343517950736398\"}");
    }
}
 
 

GOLANG

 
package main
 
import (
	"bytes"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"io/ioutil"
	"net/http"
	"strconv"
	"strings"
	"time"
)
 
const algorithm = "SHA256withRSA"
const signedHeaders = "content-type;host;x-tc-ocode;x-tc-action"
 
// 填充一下客户资料
var ocode = ""     // 客户的 appid,在管理控制台-开发者页面查看
var secretId = ""  // 客户的 secretId,在管理控制台-开发者页面查看
// 客户的私钥,private-key.pem 文件里面的内容。需提前在管理控制台-开发者页面设置好对应公钥
var privateKey = `
-----BEGIN PRIVATE KEY-----
-----END PRIVATE KEY-----
`
// 微卡收付通的公钥,在管理控制台-开发者页面查看
var publicKey = `
-----BEGIN PUBLIC KEY-----
-----END PUBLIC KEY-----
`
 
func main() {
	// 开放接口调用示例
	payload := ``      // 需改成接口实际请求body,如 {"OutOrderId": "order202211200001"}
	host := ""         // 需改成接口域名,如地址为 http://p.wecard.tencent.com/cloudpay/v1/pay/unified_order, 则 host 为 p.wecard.tencent.com
	canonicalUri := "" // 需改成接口实际uri,如地址为 http://p.wecard.tencent.com/cloudpay/v1/pay/unified_order, 则 uri 为 /cloudpay/v1/pay/unified_order
	rsp, err := CallOpenApi(host, canonicalUri, payload)
	fmt.Println(err, rsp)
}
 
// CallOpenApi 请求微卡收付通开放接口
func CallOpenApi(host, canonicalUri, payload string) (string, error) {
	// 请求参数构造
	timestamp := time.Now().Unix() // 每次请求时的秒级时间戳
	action := "wxpay"              // 固定不用修改
 
	// 获取签名
	authorization := genAuthorization(host, payload, canonicalUri, action, timestamp)
 
	// 构造请求参数
	buf := bytes.NewReader([]byte(payload))
	req, err := http.NewRequest("POST", "https://"+host+canonicalUri, buf)
	if err != nil {
		return "", fmt.Errorf("new request err:%v", err)
	}
	req.Header.Set("Authorization", authorization)
	req.Header.Set("Content-Type", "application/json; charset=utf-8")
	req.Header.Set("Host", host)
	req.Header.Set("X-TC-Action", action)
	req.Header.Set("X-TC-Timestamp", strconv.FormatInt(timestamp, 10))
	req.Header.Set("X-TC-Version", "2023-04-13")
	req.Header.Set("X-TC-Region", "ap-shanghai")
	req.Header.Set("X-TC-Ocode", ocode)
 
	// 发起请求
	rsp, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", fmt.Errorf("do request err:%v", err)
	}
	if rsp == nil {
		return "", fmt.Errorf("do request fail rsp is nil")
	}
 
	// 获取接口返回
	response, err := ioutil.ReadAll(rsp.Body)
	if err != nil {
		return "", fmt.Errorf("read body err:%v", err)
	}
	return string(response), nil
}
 
// CheckCallBackSign 验证微卡收付通回调的签名
/*
 * 回调时请求回调接口的加签逻辑,与请求开放接口的加签逻辑一致。只需要修改加签的参数即可
 * 1、准备主体的appid、secretId、PrivateKey
 * 2、从 http-header 获取 X-TC-Timestamp 计算签名的秒级时间戳(建议校验回调服务器的当前时间与该时间戳的时间差)
 * 3、获取回调接口的域名
 * 4、获取回调接口的uri
 * 5、获取回调的请求body
 * 6、将以上数据调用checkReqAuthorization 方法计算签名,与从 http-header 获取 Authorization 签名字符串比较是否相等
 */
func CheckCallBackSign(w http.ResponseWriter, r *http.Request) {
	auth := r.Header.Get("Authorization")
	if auth == "" {
		w.WriteHeader(403) // 验签失败返回的状态码不能为 200
		return
	}
	timestamp := r.Header.Get("X-TC-Timestamp")
	host := r.Host               // 回调接口的域名domain
	canonicalUri := r.RequestURI // 回调接口的路由uri
	payload, _ := ioutil.ReadAll(r.Body)
	fmt.Printf("timestamp:%s, host:%s, canonicalUri:%s,payload:%s", timestamp, host, canonicalUri, payload)
 
	timestampInt, _ := strconv.ParseInt(timestamp, 10, 64)
	signature := getSignature(auth)
	if err := verifySignature(signature, host, string(payload), canonicalUri, "Notify", timestampInt); err != nil {
		w.WriteHeader(403) // 验签失败返回的状态码不能为 200
		return
	}
 
	// todo 使用body处理业务逻辑
	w.WriteHeader(200)
	return
}
 
func verifySignature(signature, host, payload, canonicalURI, action string, timestamp int64) error {
	_, string2sign := packSignData(host, payload, canonicalURI, action, timestamp)
	pub, err := ParsePublicKey([]byte(publicKey))
	if err != nil {
		return err
	}
	b64, err := base64.StdEncoding.DecodeString(signature)
	if err != nil {
		return err
	}
 
	hashed := sha256.Sum256([]byte(string2sign))
	if err := rsa.VerifyPKCS1v15(pub, crypto.SHA256, hashed[:], b64); err != nil {
		return err
	}
	return nil
}
 
// genAuthorization 计算签名
func genAuthorization(host, payload, canonicalURI, action string, timestamp int64) string {
	credentialScope, string2sign := packSignData(host, payload, canonicalURI, action, timestamp)
 
	// step 3: sign string
	signature, err := RsaSignBase64withSha256([]byte(privateKey), []byte(string2sign))
	fmt.Println(signature, err)
 
	// step 4: build authorization
	authorization := fmt.Sprintf("%s Credential=%s/%s, SignedHeaders=%s, Signature=%s",
		algorithm,
		secretId,
		credentialScope,
		signedHeaders,
		signature)
	fmt.Println(authorization)
	return authorization
}
 
var re = regexp.MustCompile(`Signature=([^,]+)`)
 
func getSignature(input string) string {
	matches := re.FindStringSubmatch(input)
	if len(matches) > 1 {
		return matches[1]
	}
	return ""
}
 
func packSignData(host string, payload string, canonicalURI string, action string, timestamp int64) (string, string) {
	// step 1: build canonical request string
	httpRequestMethod := "POST"
	service := "wxpay"
	canonicalQueryString := ""
	canonicalHeaders := fmt.Sprintf("content-type:%s\nhost:%s\nx-tc-ocode:%s\nx-tc-action:%s\n",
		"application/json; charset=utf-8", host, ocode, strings.ToLower(action))
 
	hashedRequestPayload := sha256hex(payload)
	canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s",
		httpRequestMethod,
		canonicalURI,
		canonicalQueryString,
		canonicalHeaders,
		signedHeaders,
		hashedRequestPayload)
	fmt.Println(canonicalRequest)
 
	// step 2: build string to sign
	date := time.Unix(timestamp, 0).UTC().Format("2006-01-02")
	credentialScope := fmt.Sprintf("%s/%s/tc3_request", date, service)
	hashedCanonicalRequest := sha256hex(canonicalRequest)
	string2sign := fmt.Sprintf("%s\n%d\n%s\n%s",
		algorithm,
		timestamp,
		credentialScope,
		hashedCanonicalRequest)
	fmt.Println(string2sign)
	return credentialScope, string2sign
}
 
func sha256hex(s string) string {
	b := sha256.Sum256([]byte(s))
	return hex.EncodeToString(b[:])
}
 
// RsaSignBase64withSha256 RSA-SHA256 base64签名
func RsaSignBase64withSha256(privateKey []byte, data []byte) (string, error) {
	pri, err := ParsePrivateKey(privateKey)
	if err != nil {
		return "", err
	}
 
	hashed := sha256.Sum256(data)
	sign, err := rsa.SignPKCS1v15(rand.Reader, pri, crypto.SHA256, hashed[:])
	if err != nil {
		return "", err
	}
 
	return base64.StdEncoding.EncodeToString(sign), nil
}
 
// ParsePrivateKey 解析rsa private key
func ParsePrivateKey(privateKey []byte) (*rsa.PrivateKey, error) {
	block, _ := pem.Decode(privateKey)
	if block == nil {
		return nil, errors.New("privatekey error")
	}
 
	private, err := x509.ParsePKCS8PrivateKey(block.Bytes)
 
	if err != nil {
		return nil, err
	}
 
	return private.(*rsa.PrivateKey), nil
}
 
//ParsePublicKey 解析public key
func ParsePublicKey(pubkey []byte) (*rsa.PublicKey, error) {
	block, _ := pem.Decode(pubkey)
	if block == nil {
		return nil, errors.New("publickey error")
	}
	key, err := x509.ParsePKIXPublicKey(block.Bytes)
	if err != nil {
		return nil, err
	}
	pub, ok := key.(*rsa.PublicKey)
	if !ok {
		return nil, errors.New("publickey unknow")
	}
 
	return pub, nil
}
 
 

PHP

 
<?php
/*
 * CallOpenApi 请求开放接口通用方法
 */
function CallOpenApi()
{
    // 客户的资料
    $appId = ""; // 客户的 appid,在管理控制台-开发者页面查看
    $secretId = ""; // 客户的 secretId,在管理控制台-开发者页面查看
    // 客户的 privateKey,private-key.pem 文件里面的内容。需提前在管理控制台-开发者页面设置好对应公钥
    $privateKey = <<<TEXT
-----BEGIN PRIVATE KEY-----
-----END PRIVATE KEY-----
TEXT;
 
    // 请求参数构造
    $timestamp = time(); // 每次请求时的秒级时间戳
    $payload = ''; // 需改成接口实际请求body,如 {"OutOrderId": "order202211200001"}
    $host = ''; // 需改成接口域名,如地址为 http://p.wecard.tencent.com/cloudpay/v1/pay/unified_order, 则 host 为 p.wecard.tencent.com
    $canonicalUri = ''; // 需改成接口实际uri,如地址为 http://p.wecard.tencent.com/cloudpay/v1/pay/unified_order, 则 uri 为 /cloudpay/v1/pay/unified_order
    $action = "wxpay";  // 固定不用修改
 
    // 获取签名
    $authorization = genAuthorization($host, $appId, $payload, $canonicalUri, $action, $timestamp, $secretId, $privateKey);
 
    // 组装请求内容并发起请求
    $version = "2023-04-13"; // 固定不用修改
    $region = "ap-shanghai";  // 固定不用修改
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, "https://" . $host . $canonicalUri);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
    curl_setopt($ch, CURLOPT_HTTPHEADER, array(
        "Authorization: " . $authorization,
        "Content-Type: application/json; charset=utf-8",
        "Host: " . $host,
        "X-TC-Action: " . $action,
        "X-TC-Timestamp: " . $timestamp,
        "X-TC-Version: " . $version,
        "X-TC-Region: " . $region,
        "X-TC-Ocode: " . $appId,
    ));
    $result = curl_exec($ch);
    curl_close($ch);
    var_dump($result);
    // 根据接口定义返回体处理结果
}
 
/*
 * CheckCallBackSign 回调的验签流程
 * 回调时请求回调接口的加签逻辑,与请求开放接口的加签逻辑一致。只需要修改加签的参数即可
 * 1、准备主体的appid、secretId、微卡收付通的publicKey
 * 2、从 http-header 获取 X-TC-Timestamp 计算签名的秒级时间戳(建议校验回调服务器的当前时间与该时间戳的时间差)
 * 3、获取回调接口的域名
 * 4、获取回调接口的uri
 * 5、获取回调的请求body
 * 6、将以上数据调用checkReqAuthorization 方法计算签名,与从 http-header 获取 Authorization 签名字符串比较是否相等
 *
 */
function CheckCallBackSign()
{
    $requestAuth = $_SERVER['HTTP_AUTHORIZATION']; // 从header中获取签名,header中的key为Authorization
    if ($requestAuth == "") {
        http_response_code(400); // 验签失败返回的状态码不能为 200
        return;
    }
 
    // 客户的资料
    $appId = ""; // 客户的appid
    $secretId = ""; // 客户的secretId
    // 微卡收付通的公钥,在管理控制台-开发者页面查看
    $publicKey = <<<TEXT
-----BEGIN PUBLIC KEY-----
-----END PUBLIC KEY-----
TEXT;
 
    // 从请求数据中获取加签资料
    $timestamp = $_SERVER['HTTP_X_TC_TIMESTAMP'];   // 从header中获取X-TC-Timestamp加签使用的秒级的时间戳
    $host = $_SERVER["HTTP_HOST"];     // 回调接口的域名domain,如回调地址为http://xxx.com/t/pay_notify, 则 domain 为 xxx.com
    $canonicalUri = $_SERVER["REQUEST_URI"];   // 回调接口的路由uri,如回调地址为http://xxx.com/order/pay_notify, 则 uri 为 /order/pay_notify
    $payload = file_get_contents('php://input'); // 获取回调请求的 body
    $action = "Notify";
 
    // 正则表达式匹配 Signature= 后面的内容直到遇到逗号或字符串结束
    $signature = '';
    $pattern = '/Signature=([^,]+)/';
    if (preg_match($pattern, $requestAuth, $matches)) {
        // $matches[1] 包含我们要提取的数据
        $signature = $matches[1];
    }
    // 验证请求的签名数据,和计算出来的签名是否一致
    if (genAuthorization($host, $appId, $payload, $canonicalUri, $action, $timestamp, $secretId, "", true, $signature, $publicKey)) {
        echo "验签失败\n";
        http_response_code(403); // 验签失败返回的状态码不能为 200
        return;
    }
 
    // todo  客户的业务逻辑
 
    echo "验签成功";
    http_response_code(200); // 验签成功返回200 http状态码
}
 
// 计算签名方法
function genAuthorization($host, $appId, $payload, $canonicalUri, $action, $timestamp, $secretId, $privateKey, $isVerify = false, $signature = "", $publicKey = '')
{
    $httpRequestMethod = "POST";
    $service = "wxpay";
    $algorithm = "SHA256withRSA";
    $canonicalQueryString = "";
 
    $canonicalHeaders = implode("\n", [
        "content-type:application/json; charset=utf-8",
        "host:" . $host,
        "x-tc-ocode:" . $appId,
        "x-tc-action:" . strtolower($action),
        ""
    ]);
    $signedHeaders = implode(";", [
        "content-type",
        "host",
        "x-tc-ocode",
        "x-tc-action",
    ]);
 
    $hashedRequestPayload = hash("SHA256", $payload);
    $canonicalRequest = $httpRequestMethod . "\n"
        . $canonicalUri . "\n"
        . $canonicalQueryString . "\n"
        . $canonicalHeaders . "\n"
        . $signedHeaders . "\n"
        . $hashedRequestPayload;
    echo $canonicalRequest . PHP_EOL;
 
    // step 2: build string to sign
    $date = gmdate("Y-m-d", $timestamp);
    $credentialScope = $date . "/" . $service . "/tc3_request";
    $hashedCanonicalRequest = hash("SHA256", $canonicalRequest);
    echo "stringToSign\n";
    $stringToSign = $algorithm . "\n"
        . $timestamp . "\n"
        . $credentialScope . "\n"
        . $hashedCanonicalRequest;
    echo $stringToSign . PHP_EOL;
 
    if ($isVerify) {
        $pk = openssl_pkey_get_public($publicKey);
        return openssl_verify($stringToSign, base64_decode($signature), $pk, "sha256") === 1;
    }
 
    // step 3: sign string
    $pk = openssl_pkey_get_private($privateKey);
    openssl_sign($stringToSign, $signature, $pk, "sha256");
    $signature = base64_encode($signature);
    echo $signature . PHP_EOL;
 
    // step 4: build authorization
    $authorization = $algorithm
        . " Credential=" . $secretId . "/" . $credentialScope
        . ", SignedHeaders=" . $signedHeaders . ", Signature=" . $signature;
 
    return $authorization;
}
 

腾讯微卡收付通接口文档