请求鉴权
申请安全凭证
本文使用的安全凭证为密钥,密钥包括 SecretId 和 PrivateKey。
- 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
参数名称 | 解释 |
---|---|
HTTPRequestMethod | HTTP 请求方法(GET、POST )。此示例取值为 POST |
CanonicalURI | URI 参数。此示例取值为 /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;
}