# 概述
在小程序管理后台开启api加密后,开发者需要对原API的请求内容加密
与签名
,同时API的回包内容需要开发者验签
与解密
。支持的api可参考接口调用。
目前支持以下几种算法,可在MP管理页配置。
加密算法
- AES256_GCM
- SM4_GCM
签名算法
- RSAwithSHA256
- SM2withSM3
# API请求处理
# 加密
请求参数
参数 | 类型 | 默认值 | 必填 | 备注 |
---|---|---|---|---|
iv | string | 是 | 初始向量,为16字节base64字符串(解码后为12字节随机字符串) | |
data | string | 是 | 加密后的密文,使用base64编码 | |
authtag | string | 是 | GCM模式输出的认证信息,使用base64编码 |
GCM使用的认证数据
GCM分组模式需要设置额外认证数据(AAD)
对密文进行认证。平台统一使用 urlpath|appid|timestamp|sn
格式,字段之间使用竖线符号|
分隔。
参数 | 说明 |
---|---|
urlpath | 当前请求API的URL路径,包含URL协议信息,不包括URL参数(URL Query) |
appid | 当前小程序的Appid |
timestamp | 加密时的时间戳,需要与HTTP请求头Wechatmp-TimeStamp 的时间戳一致 |
sn | 使用的对称密钥编号,需要在MP平台密钥管理页面获取 |
data明文格式
data明文使用JSON格式,包含原API使用的URL参数与POST参数。需要额外增加三个安全字段_n,_appid,_timestamp,这些字段首字符均为下划线,与参数字段相互独立。
参数 | 类型 | 默认值 | 必填 | 备注 |
---|---|---|---|---|
data | 原字段类型 | 是 | 原请求字段,包含URL参数、POST参数,不包含AccessToken | |
_n | string | 是 | 随机字符串,推荐使用16-32字节非固定长度随机base64字符串 | |
_appid | string | 是 | 当前小程序的Appid | |
_timestamp | number | 是 | 加密时的时间戳,需要与HTTP请求头Wechatmp-TimeStamp 的时间戳一致 |
# 示例
以风控接口为例, 对原请求数据加密。
# AES256_GCM
密钥信息
{
"Sn": "fa05fe1e5bcc79b81ad5ad4b58acf787",
"Key": "otUpngOjU+nVQaWJIC3D/yMLV17RKaP6t4Ot9tbnzLY="
}
原始数据
{
"appid": "wxba6223c06417af7b",
"openid": "oEWzBfmdLqhFS2mTXCo2E4Y9gJAM",
"scene": 0,
"client_ip": "127.0.0.1",
}
加密后请求
{
"iv": "fmW/zNxXlytUZBgj",
"data": "0IDVdrPtSPF/Oe2CTXCV2vVNPbVJdJlP2WaTMQnoYLh5iCrrSNfQFh25EnStDMf0hLlVNBCZQtf9NaV0m4aRA4AAYIO7oR/Ge+4yY4EmZp5EVPB42xjScgMx5X3D4VdLCfynXIUKUtZHZvk1zmLVE3RauzJgiM1BB1CPmwcENo3MTJ0z8Vfkf5tMv54kOXobDLlV5rfqKdAX7gM/rP82DgZdt9vvZX44ipdbHIjJvw83ZXAFtvftdVw2Qd8=",
"authtag": "5qeM/2vZv+6KtScN94IpMg=="
}
加密过程数据
生成12字节随机字符串iv
base64_encode(iv) = fmW/zNxXlytUZBgj
拼接额外认证数据aad
aad = https://api.weixin.qq.com/wxa/getuserriskrank|wxba6223c06417af7b|1635927954|fa05fe1e5bcc79b81ad5ad4b58acf787
在原数据内添加安全字段,组成data
{
"_n": "o89QaPVsRu1yppIZzvSZc4",
"appid": "wxba6223c06417af7b",
"openid": "oEWzBfmdLqhFS2mTXCo2E4Y9gJAM",
"scene": 0,
"client_ip": "127.0.0.1",
"_appid": "wxba6223c06417af7b",
"_timestamp": 1635927954
}
压缩data
(可选)
{"_n":"o89QaPVsRu1yppIZzvSZc4","_appid":"wxba6223c06417af7b","_timestamp":1635927954,"appid":"wxba6223c06417af7b","openid":"oEWzBfmdLqhFS2mTXCo2E4Y9gJAM","scene":0,"client_ip":"127.0.0.1"}
计算密文enc_data
与认证信息authtag
base64_encode(enc_data) = 0IDVdrPtSPF/Oe2CTXCV2vVNPbVJdJlP2WaTMQnoYLh5iCrrSNfQFh25EnStDMf0hLlVNBCZQtf9NaV0m4aRA4AAYIO7oR/Ge+4yY4EmZp5EVPB42xjScgMx5X3D4VdLCfynXIUKUtZHZvk1zmLVE3RauzJgiM1BB1CPmwcENo3MTJ0z8Vfkf5tMv54kOXobDLlV5rfqKdAX7gM/rP82DgZdt9vvZX44ipdbHIjJvw83ZXAFtvftdVw2Qd8=
base64_encode(authtag) = 5qeM/2vZv+6KtScN94IpMg==
示例代码
// AES256_GCM
const crypto = require("crypto")
// 仅做演示,敏感信息请勿硬编码
function getCtx() {
let ctx = {
local_sym_key: "otUpngOjU+nVQaWJIC3D/yMLV17RKaP6t4Ot9tbnzLY=",
local_sym_sn: "fa05fe1e5bcc79b81ad5ad4b58acf787",
local_appid: "wxba6223c06417af7b",
url_path: "https://api.weixin.qq.com/wxa/getuserriskrank"
}
return ctx
}
function getRawReq() {
let req = {
appid: "wxba6223c06417af7b",
openid: "oEWzBfmdLqhFS2mTXCo2E4Y9gJAM",
scene: 0,
client_ip: "127.0.0.1",
}
return req
}
function getNewReq(ctx, req) {
const { local_sym_key, local_sym_sn, local_appid, url_path } = ctx // 开发者本地信息
const local_ts = Math.floor(Date.now() / 1000) //加密签名使用的统一时间戳
const nonce = crypto.randomBytes(16).toString('base64').replace(/=/g, '')
const reqex = {
_n: nonce,
_appid: local_appid,
_timestamp: local_ts
}
const real_req = Object.assign({}, reqex, req) // 生成并添加安全校验字段
const plaintext = JSON.stringify(real_req)
const aad = `${url_path}|${local_appid}|${local_ts}|${local_sym_sn}`
const real_key = Buffer.from(local_sym_key, "base64")
const real_iv = crypto.randomBytes(12)
const real_aad = Buffer.from(aad, "utf-8")
const real_plaintext = Buffer.from(plaintext, "utf-8")
const cipher = crypto.createCipheriv("aes-256-gcm", real_key, real_iv)
cipher.setAAD(real_aad)
let cipher_update = cipher.update(real_plaintext)
let cipher_final = cipher.final()
const real_ciphertext = Buffer.concat([cipher_update, cipher_final])
const real_authTag = cipher.getAuthTag()
const iv = real_iv.toString("base64")
const data = real_ciphertext.toString("base64")
const authtag = real_authTag.toString("base64")
const req_data = {
iv,
data,
authtag,
}
const new_req = {
req_ts: local_ts,
req_data: JSON.stringify(req_data)
}
return new_req
}
const ctx = getCtx()
const req = getRawReq()
let res = getNewReq(ctx, req)
console.log(res)
// AES256_GCM
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
public class AES_Enc {
private static JsonObject getReqForSign(JsonObject ctx,JsonObject req) {
Gson gson = new Gson();
// 开发者本地信息
String local_appid = ctx.get("local_appid").getAsString();
String url_path = ctx.get("url_path").getAsString();
String local_sym_sn = ctx.get("local_sym_sn").getAsString();
String local_sym_key = ctx.get("local_sym_key").getAsString();
//加密签名使用的统一时间戳
long localTs = System.currentTimeMillis() / 1000;
String nonce = generateNonce();
req.addProperty("_n",nonce);
req.addProperty("_appid",local_appid);
req.addProperty("_timestamp",localTs);
String plaintext = gson.toJson(req);
String aad = url_path + "|" + local_appid + "|" + localTs + "|" + local_sym_sn;
byte[] realKey = Base64.getDecoder().decode(local_sym_key);
byte[] realIv = generateRandomBytes(12);
byte[] realAad = aad.getBytes(StandardCharsets.UTF_8);
byte[] realPlaintext = plaintext.getBytes(StandardCharsets.UTF_8);
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(realKey, "AES");
GCMParameterSpec parameterSpec = new GCMParameterSpec(128, realIv);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, parameterSpec);
cipher.updateAAD(realAad);
byte[] ciphertext = cipher.doFinal(realPlaintext);
byte[] encryptedData = Arrays.copyOfRange(ciphertext, 0, ciphertext.length - 16);
byte[] authTag = Arrays.copyOfRange(ciphertext, ciphertext.length - 16, ciphertext.length);
String iv = base64Encode(realIv);
String data = base64Encode(encryptedData);
String authtag = base64Encode(authTag);
JsonObject reqData = new JsonObject();
reqData.addProperty("iv",iv);
reqData.addProperty("data",data);
reqData.addProperty("authtag",authtag);
JsonObject reqforsign = new JsonObject();
reqforsign.addProperty("req_ts",localTs);
reqforsign.addProperty("req_data",reqData.toString());
return reqforsign;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private static String generateNonce() {
byte[] nonce = generateRandomBytes(16);
return base64Encode(nonce).replace("=", "");
}
private static byte[] generateRandomBytes(int length) {
byte[] bytes = new byte[length];
new SecureRandom().nextBytes(bytes);
return bytes;
}
private static String base64Encode(byte[] data) {
return Base64.getEncoder().encodeToString(data);
}
private static JsonObject getCtx(){
JsonObject ctx = new JsonObject();
// 仅做演示,敏感信息请勿硬编码
ctx.addProperty("local_sym_key","otUpngOjU+nVQaWJIC3D/yMLV17RKaP6t4Ot9tbnzLY=");
ctx.addProperty("local_sym_sn","fa05fe1e5bcc79b81ad5ad4b58acf787");
ctx.addProperty("local_appid","wxba6223c06417af7b");
ctx.addProperty("url_path","https://api.weixin.qq.com/wxa/getuserriskrank");
return ctx;
}
private static JsonObject getRawReq(){
JsonObject req = new JsonObject();
req.addProperty("appid","wxba6223c06417af7b");
req.addProperty("openid","oEWzBfmdLqhFS2mTXCo2E4Y9gJAM");
req.addProperty("scene",0);
req.addProperty("client_ip","127.0.0.1");
return req;
}
public static void main(String[] args) {
JsonObject req = getRawReq();
JsonObject ctx = getCtx();
JsonObject reqforsign = getReqForSign(ctx,req);
if(reqforsign != null){
System.out.println(reqforsign.get("req_ts").getAsLong());
System.out.println(reqforsign.get("req_data").getAsString());
}
}
}
本示例代码需要用到腾讯Kona国密套件:TencentKonaSMSuite
// SM4_GCM
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.tencent.kona.KonaProvider;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.security.Security;
import java.util.Arrays;
import java.util.Base64;
public class SM4_Enc {
static{
Security.addProvider(new KonaProvider());
}
private static JsonObject getReqForSign(JsonObject ctx, JsonObject req) {
Gson gson = new Gson();
// 开发者本地信息
String local_appid = ctx.get("local_appid").getAsString();
String url_path = ctx.get("url_path").getAsString();
String local_sym_sn = ctx.get("local_sym_sn").getAsString();
String local_sym_key = ctx.get("local_sym_key").getAsString();
//加密签名使用的统一时间戳
long localTs = System.currentTimeMillis() / 1000;
String nonce = generateNonce();
req.addProperty("_n",nonce);
req.addProperty("_appid",local_appid);
req.addProperty("_timestamp",localTs);
String plaintext = gson.toJson(req);
String aad = url_path + "|" + local_appid + "|" + localTs + "|" + local_sym_sn;
byte[] realKey = Base64.getDecoder().decode(local_sym_key);
byte[] realIv = generateRandomBytes(12);
byte[] realAad = aad.getBytes(StandardCharsets.UTF_8);
byte[] realPlaintext = plaintext.getBytes(StandardCharsets.UTF_8);
try {
Cipher cipher = Cipher.getInstance("SM4/GCM/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(realKey, "SM4");
GCMParameterSpec parameterSpec = new GCMParameterSpec(128, realIv);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, parameterSpec);
cipher.updateAAD(realAad);
byte[] ciphertext = cipher.doFinal(realPlaintext);
byte[] encryptedData = Arrays.copyOfRange(ciphertext, 0, ciphertext.length - 16);
byte[] authTag = Arrays.copyOfRange(ciphertext, ciphertext.length - 16, ciphertext.length);
String iv = base64Encode(realIv);
String data = base64Encode(encryptedData);
String authtag = base64Encode(authTag);
JsonObject reqData = new JsonObject();
reqData.addProperty("iv",iv);
reqData.addProperty("data",data);
reqData.addProperty("authtag",authtag);
JsonObject reqforsign = new JsonObject();
reqforsign.addProperty("req_ts",localTs);
reqforsign.addProperty("req_data",reqData.toString());
return reqforsign;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private static String generateNonce() {
byte[] nonce = generateRandomBytes(16);
return base64Encode(nonce).replace("=", "");
}
private static byte[] generateRandomBytes(int length) {
byte[] bytes = new byte[length];
new SecureRandom().nextBytes(bytes);
return bytes;
}
private static String base64Encode(byte[] data) {
return Base64.getEncoder().encodeToString(data);
}
private static JsonObject getCtx(){
JsonObject ctx = new JsonObject();
// 仅做演示,敏感信息请勿硬编码
ctx.addProperty("local_sym_key","YXBwaWRkdXNlZm9ydGVzdA==");
ctx.addProperty("local_sym_sn","fa05fe1e5bcc79b81ad5ad4b58acf787");
ctx.addProperty("local_appid","wxba6223c06417af7b");
ctx.addProperty("url_path","https://api.weixin.qq.com/wxa/getuserriskrank");
return ctx;
}
private static JsonObject getRawReq(){
JsonObject req = new JsonObject();
req.addProperty("appid","wxba6223c06417af7b");
req.addProperty("openid","oEWzBfmdLqhFS2mTXCo2E4Y9gJAM");
req.addProperty("scene",0);
req.addProperty("client_ip","127.0.0.1");
return req;
}
public static void main(String[] args) {
JsonObject req = getRawReq();
JsonObject ctx = getCtx();
JsonObject reqforsign = getReqForSign(ctx,req);
if(reqforsign != null){
System.out.println(reqforsign.get("req_ts").getAsLong());
System.out.println(reqforsign.get("req_data").getAsString());
}
}
}
# 签名
开发者需要对API的POST数据签名,由HTTP请求头传递。
请求参数
HEADER名 | 默认值 | 必填 | 备注 |
---|---|---|---|
Wechatmp-Appid | 是 | 当前小程序的Appid | |
Wechatmp-TimeStamp | 是 | 签名时时间戳 | |
Wechatmp-Signature | 是 | 签名数据,使用base64编码 |
签名字段格式
开发者需先拼接待签名串,使用 urlpath\n appid\n timestamp\n postdata
格式,字段之间使用换行符\n
做分隔符。
参数 | 说明 |
---|---|
urlpath | 当前请求API的URL,不包括URL参数(URL Query),需要带HTTP协议头 |
appid | 当前小程序的Appid |
timestamp | 签名时的时间戳,即请求头Wechatmp-TimeStamp 的值 |
postdata | 当前请求的POST数据 |
SM2withSM3签名需要用到的ID为开发者非对称密钥编号
# 示例
以风控接口为例,对请求数据签名。
# RSAwithSHA256
签名使用PSS填充方式,需要指定salt长度为32。(PSS签名中包含随机因子,因此每次签名结果都会变化)
私钥信息
{
"Sn": "97845f6ed842ea860df6fdf65941ff56",
"PrivateKey": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA3FoQOmOl5/CF5hF7ta4EzCy2LaU3Eu2k9DBwQ73J82I53Sx9\nLAgM1DH3IsYohRRx/BESfbdDI2powvr6QYKVIC+4Yavwg7gzhZRxWWmT1HruEADC\nZAgkUCu+9Il/9FPuitPSoIpBd07NqdkkRe82NBOfrKTdhge/5zd457fl7J81Q5VT\nIxO8vvq7FSw7k6Jtv+eOjR6SZOWbbUO7f9r4UuUkXmvdGv21qiqtaO1EMw4tUCEL\nzY73M7NpCH3RorlommYX3P6q0VrkDHrCE0/QMhmHsF+46E+IRcJ3wtEj3p/mO1Vo\nCpEhawC1U728ZUTwWNEii8hPEhcNAZTKaQMaTQIDAQABAoIBAQCXv5p/a5KcyYKc\n75tfgekh5wTLKIVmDqzT0evuauyCJTouO+4z/ZNAKuzEUO0kwPDCo8s1MpkU8boV\n1Ru1M8WZNePnt65aN+ebbaAl8FRzNvltoeg9VXIUmBvYcjzhOVAE4V2jW7M8A9QU\nzUpyswuED6OeFKfOHtYk2In2IipAqhfbyc6gn7uZSWTQsoO6hGBRQ7Ejx+vgwrbx\nZKVZ7UXbPHD0lOEPraA3PH/QUeUKpNwK2NXQoBxWcR283/HxFSAjjSSsGSBKsCnw\nDN55P2FQ0HNi5YrwUNT9190NIXSeygaRy1b+D+yBfm+yE7/qXwHLZCHsjO+2tMSS\n3KGjllTBAoGBAP9FPeYNKZuu5jt9RpZwXCc9E7Iz7bmM7zws6dun6dQH0xVVWFVm\niGIu07eqyB8HNagXseFzoXLV5EQx+3DaB0bAH+ZEpHGJJpAWSLusigssFUFuTvTF\nw+rC5hxOfidMa6+93SU5pWeJb0zJF8PRDaJ3UmwlwpYubF17sT4PD6p9AoGBANz7\nRlhRSFvggJjhEMpek3OIYWrrlRNO2MVcP7i/fGNTHhrw7OHcNGRof54QZ2Y0baL7\n1vHNokbK2mnT+cQXY/gXMmcE/eV4xyRGYiIL9nBdrkLerc43EYPv+evDvgyji6+y\n4np5cKqHrS8F+YzATk82Jt9HgdI2MvfbJTkSbmgRAoGAHNPL9rPb1An/VA6Ery6H\nKaM7Gy/EE+U3ixsjWbvvqxMrIkieDh7jHftdy2sM6Hwe8hmi6+vr+pTvD0h5tbfZ\nhILj11Q/Idc0NKdflVoZyMM0r0vuvLOsuVFDPUUb+AIoUxNk6vREmpmpqQk4ltN/\n763779yfyef6MuBqFrEKut0CgYB9FfsuuOv1nfINF7EybDCZAETsiee7ozEPHnWv\ndSzK6FytMV1VSBmcEI7UgUKWVu0MifOUsiq+WcsihmvmNLtQzoioSeoSP7ix7ulT\njmP0HQMsNPI7PW67uVZFv2pPqy/Bx8dtPlqpHN3KNV6Z7q0lJ2j/kHGK9UUKidDb\nKnS2kQKBgHZ0cYzwh9YnmfXx9mimF57aQQ8aFc9yaeD5/3G2+a/FZcHtYzUdHQ7P\nPS35blD17/NnhunHhuqakbgarH/LIFMHITCVuGQT4xS34kFVjFVhiT3cHfWyBbJ6\nGbQuzzFxz/UKDDKf3/ON41k8UP20Gdvmv/+c6qQjKPayME81elus\n-----END RSA PRIVATE KEY-----"
}
原始请求
原postdata总长度324,末尾无回车符
\n
POST /wxa/getuserriskrank?access_token=ACCESS_TOKEN HTTP/1.1
Host: api.weixin.qq.com
...
Content-Length: 324
{"iv":"fmW/zNxXlytUZBgj","data":"0IDVdrPtSPF/Oe2CTXCV2vVNPbVJdJlP2WaTMQnoYLh5iCrrSNfQFh25EnStDMf0hLlVNBCZQtf9NaV0m4aRA4AAYIO7oR/Ge+4yY4EmZp5EVPB42xjScgMx5X3D4VdLCfynXIUKUtZHZvk1zmLVE3RauzJgiM1BB1CPmwcENo3MTJ0z8Vfkf5tMv54kOXobDLlV5rfqKdAX7gM/rP82DgZdt9vvZX44ipdbHIjJvw83ZXAFtvftdVw2Qd8=","authtag":"5qeM/2vZv+6KtScN94IpMg=="}
签名后请求
POST /wxa/getuserriskrank?access_token=ACCESS_TOKEN HTTP/1.1
Host: api.weixin.qq.com
...
Content-Length: 324
Wechatmp-Appid: wxba6223c06417af7b
Wechatmp-TimeStamp: 1635927954
Wechatmp-Signature: wcSSWHZunjz9VKl9q+If9deiyECXDAELfAJNZ4+5T+NhFr8zfhkwdQtlgQ7nN5xs99R57La9UjBTRBGge2KYyshWtw7HIMPAqWNsnpHvx0b2f7s6Bt7OpfOQLlIfNgepgTVmUwrqW8/7A12szj7tCe/bRFilwnaX6N0w4duHlfL7ic7IIZXouvy9dLRAa5GtEk1eD/LPWRiKh0SvJ3znPY/pSiQW9zSkXVdj9UGGM8qcKLzPGJ7gSmt3ZOPkFapk9wqFmhJwQj//xN5+hUlr2UiNPMNSHve5Y2ADLsNHqk5t7RfAZ8nW9/8lzhVt4t+toy1FeehxCGIC8qgmjIl1hg==
{"iv":"fmW/zNxXlytUZBgj","data":"0IDVdrPtSPF/Oe2CTXCV2vVNPbVJdJlP2WaTMQnoYLh5iCrrSNfQFh25EnStDMf0hLlVNBCZQtf9NaV0m4aRA4AAYIO7oR/Ge+4yY4EmZp5EVPB42xjScgMx5X3D4VdLCfynXIUKUtZHZvk1zmLVE3RauzJgiM1BB1CPmwcENo3MTJ0z8Vfkf5tMv54kOXobDLlV5rfqKdAX7gM/rP82DgZdt9vvZX44ipdbHIjJvw83ZXAFtvftdVw2Qd8=","authtag":"5qeM/2vZv+6KtScN94IpMg=="}
签名过程数据
拼接待签名串M
,末尾无额外回车符\n
https://api.weixin.qq.com/wxa/getuserriskrank
wxba6223c06417af7b
1635927954
{"iv":"fmW/zNxXlytUZBgj","data":"0IDVdrPtSPF/Oe2CTXCV2vVNPbVJdJlP2WaTMQnoYLh5iCrrSNfQFh25EnStDMf0hLlVNBCZQtf9NaV0m4aRA4AAYIO7oR/Ge+4yY4EmZp5EVPB42xjScgMx5X3D4VdLCfynXIUKUtZHZvk1zmLVE3RauzJgiM1BB1CPmwcENo3MTJ0z8Vfkf5tMv54kOXobDLlV5rfqKdAX7gM/rP82DgZdt9vvZX44ipdbHIjJvw83ZXAFtvftdVw2Qd8=","authtag":"5qeM/2vZv+6KtScN94IpMg=="}
使用PSS填充方式计算签名S
base64_encode(S) = wcSSWHZunjz9VKl9q+If9deiyECXDAELfAJNZ4+5T+NhFr8zfhkwdQtlgQ7nN5xs99R57La9UjBTRBGge2KYyshWtw7HIMPAqWNsnpHvx0b2f7s6Bt7OpfOQLlIfNgepgTVmUwrqW8/7A12szj7tCe/bRFilwnaX6N0w4duHlfL7ic7IIZXouvy9dLRAa5GtEk1eD/LPWRiKh0SvJ3znPY/pSiQW9zSkXVdj9UGGM8qcKLzPGJ7gSmt3ZOPkFapk9wqFmhJwQj//xN5+hUlr2UiNPMNSHve5Y2ADLsNHqk5t7RfAZ8nW9/8lzhVt4t+toy1FeehxCGIC8qgmjIl1hg==
示例代码
nodejs使用的rsa私钥为pkcs#1格式
// RSAwithSHA256
const crypto = require("crypto")
// 仅做演示,敏感信息请勿硬编码
function getCtx() {
let ctx = {
local_private_key: "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA3FoQOmOl5/CF5hF7ta4EzCy2LaU3Eu2k9DBwQ73J82I53Sx9\nLAgM1DH3IsYohRRx/BESfbdDI2powvr6QYKVIC+4Yavwg7gzhZRxWWmT1HruEADC\nZAgkUCu+9Il/9FPuitPSoIpBd07NqdkkRe82NBOfrKTdhge/5zd457fl7J81Q5VT\nIxO8vvq7FSw7k6Jtv+eOjR6SZOWbbUO7f9r4UuUkXmvdGv21qiqtaO1EMw4tUCEL\nzY73M7NpCH3RorlommYX3P6q0VrkDHrCE0/QMhmHsF+46E+IRcJ3wtEj3p/mO1Vo\nCpEhawC1U728ZUTwWNEii8hPEhcNAZTKaQMaTQIDAQABAoIBAQCXv5p/a5KcyYKc\n75tfgekh5wTLKIVmDqzT0evuauyCJTouO+4z/ZNAKuzEUO0kwPDCo8s1MpkU8boV\n1Ru1M8WZNePnt65aN+ebbaAl8FRzNvltoeg9VXIUmBvYcjzhOVAE4V2jW7M8A9QU\nzUpyswuED6OeFKfOHtYk2In2IipAqhfbyc6gn7uZSWTQsoO6hGBRQ7Ejx+vgwrbx\nZKVZ7UXbPHD0lOEPraA3PH/QUeUKpNwK2NXQoBxWcR283/HxFSAjjSSsGSBKsCnw\nDN55P2FQ0HNi5YrwUNT9190NIXSeygaRy1b+D+yBfm+yE7/qXwHLZCHsjO+2tMSS\n3KGjllTBAoGBAP9FPeYNKZuu5jt9RpZwXCc9E7Iz7bmM7zws6dun6dQH0xVVWFVm\niGIu07eqyB8HNagXseFzoXLV5EQx+3DaB0bAH+ZEpHGJJpAWSLusigssFUFuTvTF\nw+rC5hxOfidMa6+93SU5pWeJb0zJF8PRDaJ3UmwlwpYubF17sT4PD6p9AoGBANz7\nRlhRSFvggJjhEMpek3OIYWrrlRNO2MVcP7i/fGNTHhrw7OHcNGRof54QZ2Y0baL7\n1vHNokbK2mnT+cQXY/gXMmcE/eV4xyRGYiIL9nBdrkLerc43EYPv+evDvgyji6+y\n4np5cKqHrS8F+YzATk82Jt9HgdI2MvfbJTkSbmgRAoGAHNPL9rPb1An/VA6Ery6H\nKaM7Gy/EE+U3ixsjWbvvqxMrIkieDh7jHftdy2sM6Hwe8hmi6+vr+pTvD0h5tbfZ\nhILj11Q/Idc0NKdflVoZyMM0r0vuvLOsuVFDPUUb+AIoUxNk6vREmpmpqQk4ltN/\n763779yfyef6MuBqFrEKut0CgYB9FfsuuOv1nfINF7EybDCZAETsiee7ozEPHnWv\ndSzK6FytMV1VSBmcEI7UgUKWVu0MifOUsiq+WcsihmvmNLtQzoioSeoSP7ix7ulT\njmP0HQMsNPI7PW67uVZFv2pPqy/Bx8dtPlqpHN3KNV6Z7q0lJ2j/kHGK9UUKidDb\nKnS2kQKBgHZ0cYzwh9YnmfXx9mimF57aQQ8aFc9yaeD5/3G2+a/FZcHtYzUdHQ7P\nPS35blD17/NnhunHhuqakbgarH/LIFMHITCVuGQT4xS34kFVjFVhiT3cHfWyBbJ6\nGbQuzzFxz/UKDDKf3/ON41k8UP20Gdvmv/+c6qQjKPayME81elus\n-----END RSA PRIVATE KEY-----",
local_sn: "97845f6ed842ea860df6fdf65941ff56",
local_appid: "wxba6223c06417af7b",
url_path: "https://api.weixin.qq.com/wxa/getuserriskrank"
}
return ctx
}
function getReq() {
let req = {
req_ts: 1635927954,
req_data: '{"iv":"fmW/zNxXlytUZBgj","data":"0IDVdrPtSPF/Oe2CTXCV2vVNPbVJdJlP2WaTMQnoYLh5iCrrSNfQFh25EnStDMf0hLlVNBCZQtf9NaV0m4aRA4AAYIO7oR/Ge+4yY4EmZp5EVPB42xjScgMx5X3D4VdLCfynXIUKUtZHZvk1zmLVE3RauzJgiM1BB1CPmwcENo3MTJ0z8Vfkf5tMv54kOXobDLlV5rfqKdAX7gM/rP82DgZdt9vvZX44ipdbHIjJvw83ZXAFtvftdVw2Qd8=","authtag":"5qeM/2vZv+6KtScN94IpMg=="}'
}
return req
}
function getSignature(ctx, req) {
const { local_private_key, local_sn, local_appid, url_path } = ctx // 开发者本地信息
const { req_ts, req_data } = req // 待请求API数据
const payload = `${url_path}\n${local_appid}\n${req_ts}\n${req_data}`
const data_buffer = Buffer.from(payload, 'utf-8')
const key_obj = {
key: local_private_key,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST // salt长度,需与SHA256结果长度(32)一致
}
const sig_buffer = ss_buffer = crypto.sign(
'RSA-SHA256',
data_buffer,
key_obj
)
const sig = sig_buffer.toString('base64')
return sig
/*
最终请求头字段
{
"Wechatmp-Appid": local_appid,
"Wechatmp-TimeStamp": req_ts,
"Wechatmp-Signature": sig,
}
*/
}
const ctx = getCtx()
const req = getReq()
let res = getSignature(ctx, req)
console.log(res)
java使用的rsa私钥为pkcs#8格式,转换命令:
openssl pkcs8 -topk8 -inform PEM -outform PEM -in pkcs1_key.pem -out pkcs8_key.pem -nocrypt
// RSAwithSHA256
import com.google.gson.JsonObject;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.Signature;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.MGF1ParameterSpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.PSSParameterSpec;
import java.util.Base64;
public class RSA_Sign {
public static String getSignature(JsonObject ctx, JsonObject req){
String signatureBase64 = null;
// 开发者本地信息
String local_appid = ctx.get("local_appid").getAsString();
String url_path = ctx.get("url_path").getAsString();
String local_sym_sn = ctx.get("local_sym_sn").getAsString();
String local_private_key = ctx.get("local_private_key").getAsString();
// 待请求API数据
long reqTs = req.get("req_ts").getAsLong();
String reqData = req.get("req_data").getAsString();
String payload = url_path + "\n" + local_appid + "\n" + reqTs + "\n" + reqData;
byte[] dataBuffer = payload.getBytes(StandardCharsets.UTF_8);
try{
local_private_key = local_private_key.replace("-----BEGIN PRIVATE KEY-----", "");
local_private_key = local_private_key.replace("-----END PRIVATE KEY-----", "");
local_private_key = local_private_key.replaceAll("\\s+","");
byte[] decoded = Base64.getDecoder().decode(local_private_key);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decoded);
RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(keySpec);
Signature signature = Signature.getInstance("RSASSA-PSS");
// salt长度,需与SHA256结果长度(32)一致
PSSParameterSpec pssParameterSpec = new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1);
signature.setParameter(pssParameterSpec);
signature.initSign(priKey);
signature.update(dataBuffer);
byte[] sigBuffer = signature.sign();
signatureBase64 = Base64.getEncoder().encodeToString(sigBuffer);
}catch (Exception e){
e.printStackTrace();
}
return signatureBase64;
/*
最终请求头字段
{
"Wechatmp-Appid": local_appid,
"Wechatmp-TimeStamp": req_ts,
"Wechatmp-Signature": sig,
}
*/
}
private static JsonObject getReq(){
JsonObject req = new JsonObject();
req.addProperty("req_ts",1635927954);
req.addProperty("req_data","{\"iv\":\"fmW/zNxXlytUZBgj\",\"data\":\"0IDVdrPtSPF/Oe2CTXCV2vVNPbVJdJlP2WaTMQnoYLh5iCrrSNfQFh25EnStDMf0hLlVNBCZQtf9NaV0m4aRA4AAYIO7oR/Ge+4yY4EmZp5EVPB42xjScgMx5X3D4VdLCfynXIUKUtZHZvk1zmLVE3RauzJgiM1BB1CPmwcENo3MTJ0z8Vfkf5tMv54kOXobDLlV5rfqKdAX7gM/rP82DgZdt9vvZX44ipdbHIjJvw83ZXAFtvftdVw2Qd8=\",\"authtag\":\"5qeM/2vZv+6KtScN94IpMg==\"}");
return req;
}
private static JsonObject getCtx(){
JsonObject ctx = new JsonObject();
// 仅做演示,敏感信息请勿硬编码
String localPrivateKey = "-----BEGIN PRIVATE KEY-----\n" +
"MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDcWhA6Y6Xn8IXm\n" +
"EXu1rgTMLLYtpTcS7aT0MHBDvcnzYjndLH0sCAzUMfcixiiFFHH8ERJ9t0MjamjC\n" +
"+vpBgpUgL7hhq/CDuDOFlHFZaZPUeu4QAMJkCCRQK770iX/0U+6K09KgikF3Ts2p\n" +
"2SRF7zY0E5+spN2GB7/nN3jnt+XsnzVDlVMjE7y++rsVLDuTom2/546NHpJk5Ztt\n" +
"Q7t/2vhS5SRea90a/bWqKq1o7UQzDi1QIQvNjvczs2kIfdGiuWiaZhfc/qrRWuQM\n" +
"esITT9AyGYewX7joT4hFwnfC0SPen+Y7VWgKkSFrALVTvbxlRPBY0SKLyE8SFw0B\n" +
"lMppAxpNAgMBAAECggEBAJe/mn9rkpzJgpzvm1+B6SHnBMsohWYOrNPR6+5q7IIl\n" +
"Oi477jP9k0Aq7MRQ7STA8MKjyzUymRTxuhXVG7UzxZk14+e3rlo355ttoCXwVHM2\n" +
"+W2h6D1VchSYG9hyPOE5UAThXaNbszwD1BTNSnKzC4QPo54Up84e1iTYifYiKkCq\n" +
"F9vJzqCfu5lJZNCyg7qEYFFDsSPH6+DCtvFkpVntRds8cPSU4Q+toDc8f9BR5Qqk\n" +
"3ArY1dCgHFZxHbzf8fEVICONJKwZIEqwKfAM3nk/YVDQc2LlivBQ1P3X3Q0hdJ7K\n" +
"BpHLVv4P7IF+b7ITv+pfActkIeyM77a0xJLcoaOWVMECgYEA/0U95g0pm67mO31G\n" +
"lnBcJz0TsjPtuYzvPCzp26fp1AfTFVVYVWaIYi7Tt6rIHwc1qBex4XOhctXkRDH7\n" +
"cNoHRsAf5kSkcYkmkBZIu6yKCywVQW5O9MXD6sLmHE5+J0xrr73dJTmlZ4lvTMkX\n" +
"w9ENondSbCXCli5sXXuxPg8Pqn0CgYEA3PtGWFFIW+CAmOEQyl6Tc4hhauuVE07Y\n" +
"xVw/uL98Y1MeGvDs4dw0ZGh/nhBnZjRtovvW8c2iRsraadP5xBdj+BcyZwT95XjH\n" +
"JEZiIgv2cF2uQt6tzjcRg+/568O+DKOLr7LienlwqoetLwX5jMBOTzYm30eB0jYy\n" +
"99slORJuaBECgYAc08v2s9vUCf9UDoSvLocpozsbL8QT5TeLGyNZu++rEysiSJ4O\n" +
"HuMd+13LawzofB7yGaLr6+v6lO8PSHm1t9mEguPXVD8h1zQ0p1+VWhnIwzSvS+68\n" +
"s6y5UUM9RRv4AihTE2Tq9ESamampCTiW03/vrfvv3J/J5/oy4GoWsQq63QKBgH0V\n" +
"+y646/Wd8g0XsTJsMJkAROyJ57ujMQ8eda91LMroXK0xXVVIGZwQjtSBQpZW7QyJ\n" +
"85SyKr5ZyyKGa+Y0u1DOiKhJ6hI/uLHu6VOOY/QdAyw08js9bru5VkW/ak+rL8HH\n" +
"x20+Wqkc3co1XpnurSUnaP+QcYr1RQqJ0NsqdLaRAoGAdnRxjPCH1ieZ9fH2aKYX\n" +
"ntpBDxoVz3Jp4Pn/cbb5r8Vlwe1jNR0dDs89LfluUPXv82eG6ceG6pqRuBqsf8sg\n" +
"UwchMJW4ZBPjFLfiQVWMVWGJPdwd9bIFsnoZtC7PMXHP9QoMMp/f843jWTxQ/bQZ\n" +
"2+a//5zqpCMo9rIwTzV6W6w=\n" +
"-----END PRIVATE KEY-----";
ctx.addProperty("local_private_key",localPrivateKey);
ctx.addProperty("local_sym_sn","97845f6ed842ea860df6fdf65941ff56");
ctx.addProperty("local_appid","wxba6223c06417af7b");
ctx.addProperty("url_path","https://api.weixin.qq.com/wxa/getuserriskrank");
return ctx;
}
public static void main(String[] args) {
JsonObject req = getReq();
JsonObject ctx = getCtx();
String res = getSignature(ctx,req);
System.out.println(res);
}
}
本示例代码需要用到腾讯Kona国密套件:TencentKonaSMSuite
// SM2withSM3
import com.google.gson.JsonObject;
import com.tencent.kona.KonaProvider;
import com.tencent.kona.crypto.spec.SM2SignatureParameterSpec;
import java.nio.charset.StandardCharsets;
import java.security.PublicKey;
import java.security.PrivateKey;
import java.security.Security;
import java.security.Signature;
import java.security.KeyFactory;
import java.security.interfaces.ECPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class SM2_Sign {
static {
Security.addProvider(new KonaProvider());
}
public static String getSignature(JsonObject ctx, JsonObject req){
String signatureBase64 = null;
// 开发者本地信息
String local_appid = ctx.get("local_appid").getAsString();
String url_path = ctx.get("url_path").getAsString();
String local_sym_sn = ctx.get("local_sym_sn").getAsString();
String local_private_key = ctx.get("local_private_key").getAsString();
String local_public_key = ctx.get("local_public_key").getAsString();
// 待请求API数据
long reqTs = req.get("req_ts").getAsLong();
String reqData = req.get("req_data").getAsString();
String payload = url_path + "\n" + local_appid + "\n" + reqTs + "\n" + reqData;
byte[] dataBuffer = payload.getBytes(StandardCharsets.UTF_8);
try{
//解析私钥
local_private_key = local_private_key.replace("-----BEGIN PRIVATE KEY-----", "");
local_private_key = local_private_key.replace("-----END PRIVATE KEY-----", "");
local_private_key = local_private_key.replaceAll("\\s+","");
byte[] decoded_pri = Base64.getDecoder().decode(local_private_key);
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(decoded_pri);
PrivateKey privatekey = KeyFactory.getInstance("EC",KonaProvider.NAME).generatePrivate(privateKeySpec);
//解析公钥
local_public_key = local_public_key.replace("-----BEGIN PUBLIC KEY-----", "");
local_public_key = local_public_key.replace("-----END PUBLIC KEY-----", "");
local_public_key = local_public_key.replaceAll("\\s+","");
byte[] decoded_pub = Base64.getDecoder().decode(local_public_key);
X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(decoded_pub);
PublicKey publickey = KeyFactory.getInstance("EC",KonaProvider.NAME).generatePublic(publicKeySpec);
Signature sig = Signature.getInstance("SM2");
SM2SignatureParameterSpec paramSpec = new SM2SignatureParameterSpec(local_sym_sn.getBytes(), (ECPublicKey) publickey);
sig.setParameter(paramSpec);
sig.initSign(privatekey);
sig.update(dataBuffer);
byte[] sigBuffer = sig.sign();
signatureBase64 = Base64.getEncoder().encodeToString(sigBuffer);
}catch (Exception e){
e.printStackTrace();
}
return signatureBase64;
/*
最终请求头字段
{
"Wechatmp-Appid": local_appid,
"Wechatmp-TimeStamp": req_ts,
"Wechatmp-Signature": sig,
}
*/
}
private static JsonObject getReq(){
JsonObject req = new JsonObject();
req.addProperty("req_ts",1692932963);
req.addProperty("req_data","{\"iv\":\"b3sjwc9yvUEGH45l\",\"data\":\"fa8VugXI8UA2ugS646ZvuX0wo4qn0Eua2J9jtwACQXeVys3hP/fZDcZC4eEF9es/z/Zx6GM2piwoHKPmPbwzfNXWc/rUH/USFoKo6OBSiR8bb6QgkzYzYL9KsawMr8X/z6y8o3UzE5w65nfTySQFSpEVplD5S+SwQrLi3I2nUwS5N3SoJYsf8BHVfsYLBI9h1NocLgfjjyPYmeKsQ/t1muVWlV2Z75VbqFhM+ECgHpEvcWPDeUN5ZhZ6C/0=\",\"authtag\":\"cDZY4giOZgf73/CvObhypQ==\"}");
System.out.println(req.toString());
return req;
}
private static JsonObject getCtx(){
JsonObject ctx = new JsonObject();
// 仅做演示,敏感信息请勿硬编码
String localPrivateKey = "-----BEGIN PRIVATE KEY-----\n" +
"MEECAQAwEwYHKoZIzj0CAQYIKoEcz1UBgi0EJzAlAgEBBCC59GVvT/2lzx0nJ98r\n" +
"UaEk2kGRA33V3hw1HLcy3x8/BA==" +
"-----END PRIVATE KEY-----";
String localPublicKey = "-----BEGIN PUBLIC KEY-----\n" +
"MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEFzc+mfwshF7tv3hUEdgye8LcMr6d\n" +
"ZcBDXFAcQRJouSCJndP7mHZsV1j54Yy3Ewtan09HMhce9cW7BJjCKoV2Mg==\n" +
"-----END PUBLIC KEY-----";
ctx.addProperty("local_private_key",localPrivateKey);
ctx.addProperty("local_public_key",localPublicKey);
ctx.addProperty("local_sym_sn","8a98f6bba1415c0c4f6879bda6807861");
ctx.addProperty("local_appid","wxba6223c06417af7b");
ctx.addProperty("url_path","https://api.weixin.qq.com/wxa/getuserriskrank");
return ctx;
}
public static void main(String[] args) {
JsonObject req = getReq();
JsonObject ctx = getCtx();
String res = getSignature(ctx,req);
System.out.println(res);
}
}
# API响应处理
API响应数据需要验签与解密。
响应内的签名算法为RSAwithSHA256,验签需要使用MP平台证书
验证,可在MP管理页下载最新平台证书
。
响应内的加密算法、密钥与请求时一致。
# 验签
由于平台证书存在有效期,平台证书可能过期。在平台证书更换周期内,平台会同时带上最新证书与即将过期证书的签名结果。开发者需要根据已下载的平台证书编号
找到对应的签名来验证。
若发现使用的平台证书编号与响应内的Wechatmp-Serial-Deprecated字段匹配(即当前证书即将过期),请尽快更新MP平台证书。
请求参数
HEADER名 | 默认值 | 必填 | 备注 |
---|---|---|---|
Wechatmp-Appid | 是 | 当前小程序的Appid | |
Wechatmp-TimeStamp | 是 | 签名时时间戳 | |
Wechatmp-Serial | 是 | 平台证书编号,在MP管理页面获取,非证书内序列号 | |
Wechatmp-Signature | 是 | 平台证书签名数据,使用base64编码 | |
Wechatmp-Serial-Deprecated | 否 | 即将失效的平台证书编号,非证书内序列号,仅在证书更换周期内出现 | |
Wechatmp-Signature-Deprecated | 否 | 即将失效的平台证书签名数据,仅在证书更换周期内出现,使用base64编码 |
签名字段格式
开发者需先拼接待签名串,使用 urlpath\n appid\n timestamp\n respdata
格式,字段之间使用换行符\n
做分隔符。
参数 | 说明 |
---|---|
urlpath | 当前请求API的URL,不包括URL参数(URL Query),需要带HTTP协议头 |
appid | 当前小程序的Appid |
timestamp | 签名时的时间戳,即响应头Wechatmp-TimeStamp 的值 |
respdata | 当前响应的数据 |
SM2withSM3验签需要用到的ID为平台证书编号
# 示例
以风控接口为例,对服务端回包内容验签。
# RSAwithSHA256
响应的签名也使用PSS填充方式,一般不需要指定salt长度。
证书信息
所有参与签名的编号都在MP密钥管理页面获取,非证书内置序列号
{
"Sn": "79ba700ea147819f640941bceb38b1d1",
"Certificate": "-----BEGIN CERTIFICATE-----\nMIID0jCCArqgAwIBAgIUeE+Yy7vM/o+eHHsfM+1bGJJEZTQwDQYJKoZIhvcNAQEL\nBQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT\nFFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg\nQ0EwHhcNMjIwOTA1MDgzOTIyWhcNMjcwOTA0MDgzOTIyWjBkMRswGQYDVQQDDBJ3\neGQ5MzBlYTVkNWEyNThmNGYxFTATBgNVBAoMDFRlbmNlbnQgSW5jLjEOMAwGA1UE\nCwwFV3hnTXAxCzAJBgNVBAYMAkNOMREwDwYDVQQHDAhTaGVuWmhlbjCCASIwDQYJ\nKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM5D9qlkCmk1kr3FpF0e9pc3kGsvz5RA\n0/YRny9xPKIyV2UVMDZvRQ+mDHsiQQFE6etg457KFYSxTDKtItbdl6hJQVGeAvg0\nmqPYE9SkHRGTfL/AnXRbKBG2GC2OcaPSAprsLOersjay2me+9pF8VHybV8aox78A\nNsU75G/OO3V1iEE0s5Pmglqk8DEiw9gB/dGJzsNfXwzvyJyiUP9ZujYexyjsS+/Z\nGdSOUkqL/th+16yHj8alcdyga6YGfWEDyWkt/i/B28cwx4nzwk8xgrurifPaLuMk\n0+9wJQLCfAn/f7zyHrC8PcD1XvvRt9VBNMBASXs3710ODyyVf2lkMgkCAwEAAaOB\ngTB/MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgTwMGUGA1UdHwReMFwwWqBYoFaGVGh0\ndHA6Ly9ldmNhLml0cnVzLmNvbS5jbi9wdWJsaWMvaXRydXNjcmw/Q0E9MUJENDIy\nMEU1MERCQzA0QjA2QUQzOTc1NDk4NDZDMDFDM0U4RUJEMjANBgkqhkiG9w0BAQsF\nAAOCAQEAL2MK9tYu+ljLVBlSbfEeaKyF07TN+G31Ya5NBzeS1ZCx4joUEIyACWmG\nfUkKNKiKV+EMzxeEhKRso1Qif3E7Ipl+PQBoQw6OSR/jFHciYurnGR9CLkL03Zo1\nqw1Xetv9OipsvlpA0SOWc207e/XpGdm8C7FMXM6bzvVp8I/STTjC1vqjIZu9WavI\nRgGM4jyAPz2XogUq0BNijef8BXbbav9fAsXjHSwn5BQv4iLms3fiLm/eoyQ6dZ2R\noTudrlcyr1bG4vwETLmHF+3yfVp9dpvJ+lyfiviwDwyfa8t2WlJm27DuF4vWoxir\nmjgj9tDutIFqxLIovLyg3uiAYtSQ/Q==\n-----END CERTIFICATE-----"
}
原始响应
响应数据总长度292,末尾无回车符
\n
HTTP/1.1 200 OK
...
Content-Length: 292
Wechatmp-Appid: wxba6223c06417af7b
Wechatmp-TimeStamp: 1635927956
Wechatmp-Serial: 79ba700ea147819f640941bceb38b1d1
Wechatmp-Signature: Ht0VfQkkEweJ4hU266C14Aj64H9AXfkwNi5zxUZETCvR2svU1ZYdosDhFX/voLj1TyszqKsVxAlENGt7PPZZ8RQX7jnA4SKhiPUhW4LTbyTenisHJ+ohSfDjYnXavjQsBHspFS+BlPHuSSJ2xyQzw1+HuC6nid09ZL4FnGSYo4OI5MJrSb9xLzIVZMIDuUQchGKi/KaB1KzxECLEZcfjqbAgmxC7qOmuBLyO1WkHYDM95NJrHJWba5xv4wrwPru9yYTJSNRnlM+zrW5w9pOubC4Jtj3szTAEuOz9AcqUmgaAvMLNAIa8hfODLRe3n/cu4SgYlN/ZkNRU4QXVNbPGMg==
Wechatmp-Serial-Deprecated: 2171af9cdf1d7404423852e7e183d852
Wechatmp-Signature-Deprecated: ZP1OODikAOePc+YJUMLxunF6xV05kextO/T1fy5lWv/CwV6OCsPBRM2xRRCi+B4lYXbbfYDdjzCz5BIAWEwIdjMlg/IHcJVHhRNAlKt5A3zvzfaJa5IJQel7xuUEXk/B6KVyEb41PbzrptjUGqWyTFMrjxQ4ThJfCuYocnUng7OuDU95enMqK2hZpO8o7kFW638BAwKDSiFNEwEJDWYkLz0kEw7ma3keezm4YHYKfJmjChK39tmZld7Rw/yrV1U9RiL/DO5ayP9VmrQkT/vYrPKyqI4/xKrIaTq44jFYTPIJKdU2OnLt6kjqwp2hvCzMuJdjRcrvzhWJ2A8xZ5hI2w==
{"iv":"r2WDQt56rEAmMuoR","data":"HExs66Ik3el+iM4IpeQ7SMEN934FRLFYOd3EmeaIrpP4EPTHckoco6O+PaoRZRa3lqaPRZT7r52f7LUok6gLxc6cdR8C4vpIIfh4xfLC4L7FNy9GbuMK1hcoi8b7gkWJcwZMkuCFNEDmqn3T49oWzAQOrY4LZnnnykv6oUJotdAsnKvmoJkLK7hRh7M2B1d2UnTnRuoIyarXc5Iojwoghx4BOvnV","authtag":"z2BFD8QctKXTuBlhICGOjQ=="}
验签过程数据
拼接待签名串M
,末尾无额外回车符\n
https://api.weixin.qq.com/wxa/getuserriskrank
wxba6223c06417af7b
1635927956
{"iv":"r2WDQt56rEAmMuoR","data":"HExs66Ik3el+iM4IpeQ7SMEN934FRLFYOd3EmeaIrpP4EPTHckoco6O+PaoRZRa3lqaPRZT7r52f7LUok6gLxc6cdR8C4vpIIfh4xfLC4L7FNy9GbuMK1hcoi8b7gkWJcwZMkuCFNEDmqn3T49oWzAQOrY4LZnnnykv6oUJotdAsnKvmoJkLK7hRh7M2B1d2UnTnRuoIyarXc5Iojwoghx4BOvnV","authtag":"z2BFD8QctKXTuBlhICGOjQ=="}
计算签名串M
原始哈希值H0
hex(H0) = f797cafd9e323df336fb427569fbe67e20d5bc96dd68a3f54d66b54e6e08bb27
根据平台证书编号
获取签名数据,并使用验签接口校验签名
base64_encode(S) = Ht0VfQkkEweJ4hU266C14Aj64H9AXfkwNi5zxUZETCvR2svU1ZYdosDhFX/voLj1TyszqKsVxAlENGt7PPZZ8RQX7jnA4SKhiPUhW4LTbyTenisHJ+ohSfDjYnXavjQsBHspFS+BlPHuSSJ2xyQzw1+HuC6nid09ZL4FnGSYo4OI5MJrSb9xLzIVZMIDuUQchGKi/KaB1KzxECLEZcfjqbAgmxC7qOmuBLyO1WkHYDM95NJrHJWba5xv4wrwPru9yYTJSNRnlM+zrW5w9pOubC4Jtj3szTAEuOz9AcqUmgaAvMLNAIa8hfODLRe3n/cu4SgYlN/ZkNRU4QXVNbPGMg==
示例代码
// RSAwithSHA256
const crypto = require("crypto")
// 仅做演示,敏感信息请勿硬编码
function getCtx() {
let ctx = {
local_certificate: "-----BEGIN CERTIFICATE-----\nMIID0jCCArqgAwIBAgIUeE+Yy7vM/o+eHHsfM+1bGJJEZTQwDQYJKoZIhvcNAQEL\nBQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT\nFFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg\nQ0EwHhcNMjIwOTA1MDgzOTIyWhcNMjcwOTA0MDgzOTIyWjBkMRswGQYDVQQDDBJ3\neGQ5MzBlYTVkNWEyNThmNGYxFTATBgNVBAoMDFRlbmNlbnQgSW5jLjEOMAwGA1UE\nCwwFV3hnTXAxCzAJBgNVBAYMAkNOMREwDwYDVQQHDAhTaGVuWmhlbjCCASIwDQYJ\nKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM5D9qlkCmk1kr3FpF0e9pc3kGsvz5RA\n0/YRny9xPKIyV2UVMDZvRQ+mDHsiQQFE6etg457KFYSxTDKtItbdl6hJQVGeAvg0\nmqPYE9SkHRGTfL/AnXRbKBG2GC2OcaPSAprsLOersjay2me+9pF8VHybV8aox78A\nNsU75G/OO3V1iEE0s5Pmglqk8DEiw9gB/dGJzsNfXwzvyJyiUP9ZujYexyjsS+/Z\nGdSOUkqL/th+16yHj8alcdyga6YGfWEDyWkt/i/B28cwx4nzwk8xgrurifPaLuMk\n0+9wJQLCfAn/f7zyHrC8PcD1XvvRt9VBNMBASXs3710ODyyVf2lkMgkCAwEAAaOB\ngTB/MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgTwMGUGA1UdHwReMFwwWqBYoFaGVGh0\ndHA6Ly9ldmNhLml0cnVzLmNvbS5jbi9wdWJsaWMvaXRydXNjcmw/Q0E9MUJENDIy\nMEU1MERCQzA0QjA2QUQzOTc1NDk4NDZDMDFDM0U4RUJEMjANBgkqhkiG9w0BAQsF\nAAOCAQEAL2MK9tYu+ljLVBlSbfEeaKyF07TN+G31Ya5NBzeS1ZCx4joUEIyACWmG\nfUkKNKiKV+EMzxeEhKRso1Qif3E7Ipl+PQBoQw6OSR/jFHciYurnGR9CLkL03Zo1\nqw1Xetv9OipsvlpA0SOWc207e/XpGdm8C7FMXM6bzvVp8I/STTjC1vqjIZu9WavI\nRgGM4jyAPz2XogUq0BNijef8BXbbav9fAsXjHSwn5BQv4iLms3fiLm/eoyQ6dZ2R\noTudrlcyr1bG4vwETLmHF+3yfVp9dpvJ+lyfiviwDwyfa8t2WlJm27DuF4vWoxir\nmjgj9tDutIFqxLIovLyg3uiAYtSQ/Q==\n-----END CERTIFICATE-----",
local_sn: "79ba700ea147819f640941bceb38b1d1",
local_appid: "wxba6223c06417af7b",
url_path: "https://api.weixin.qq.com/wxa/getuserriskrank"
}
return ctx
}
function getResp() {
let resp = {
resp_appid: "wxba6223c06417af7b",
resp_ts: 1635927956,
resp_sn: "79ba700ea147819f640941bceb38b1d1",
resp_sig: "Ht0VfQkkEweJ4hU266C14Aj64H9AXfkwNi5zxUZETCvR2svU1ZYdosDhFX/voLj1TyszqKsVxAlENGt7PPZZ8RQX7jnA4SKhiPUhW4LTbyTenisHJ+ohSfDjYnXavjQsBHspFS+BlPHuSSJ2xyQzw1+HuC6nid09ZL4FnGSYo4OI5MJrSb9xLzIVZMIDuUQchGKi/KaB1KzxECLEZcfjqbAgmxC7qOmuBLyO1WkHYDM95NJrHJWba5xv4wrwPru9yYTJSNRnlM+zrW5w9pOubC4Jtj3szTAEuOz9AcqUmgaAvMLNAIa8hfODLRe3n/cu4SgYlN/ZkNRU4QXVNbPGMg==",
resp_deprecated_sn: "2171af9cdf1d7404423852e7e183d852",
resp_deprecated_sig: "ZP1OODikAOePc+YJUMLxunF6xV05kextO/T1fy5lWv/CwV6OCsPBRM2xRRCi+B4lYXbbfYDdjzCz5BIAWEwIdjMlg/IHcJVHhRNAlKt5A3zvzfaJa5IJQel7xuUEXk/B6KVyEb41PbzrptjUGqWyTFMrjxQ4ThJfCuYocnUng7OuDU95enMqK2hZpO8o7kFW638BAwKDSiFNEwEJDWYkLz0kEw7ma3keezm4YHYKfJmjChK39tmZld7Rw/yrV1U9RiL/DO5ayP9VmrQkT/vYrPKyqI4/xKrIaTq44jFYTPIJKdU2OnLt6kjqwp2hvCzMuJdjRcrvzhWJ2A8xZ5hI2w==",
resp_data: '{"iv":"r2WDQt56rEAmMuoR","data":"HExs66Ik3el+iM4IpeQ7SMEN934FRLFYOd3EmeaIrpP4EPTHckoco6O+PaoRZRa3lqaPRZT7r52f7LUok6gLxc6cdR8C4vpIIfh4xfLC4L7FNy9GbuMK1hcoi8b7gkWJcwZMkuCFNEDmqn3T49oWzAQOrY4LZnnnykv6oUJotdAsnKvmoJkLK7hRh7M2B1d2UnTnRuoIyarXc5Iojwoghx4BOvnV","authtag":"z2BFD8QctKXTuBlhICGOjQ=="}'
}
return resp
}
function checkSignature(ctx, resp) {
const { local_certificate, local_sn, local_appid, url_path } = ctx // 开发者本地信息
const { resp_appid, resp_ts, resp_sn, resp_sig, resp_deprecated_sn, resp_deprecated_sig, resp_data } = resp // API响应数据,包括响应头与响应数据
const local_ts = Math.floor(Date.now() / 1000)
// 安全检查,根据业务实际需求判断
if (local_appid != resp_appid || // 回包appid不正确
local_ts - resp_ts > 300) { // 回包时间超过5分钟
console.error("安全字段校验失败")
return false
}
let signature = ''
if (local_sn === resp_sn) {
signature = resp_sig
} else if (local_sn === resp_deprecated_sn) { // 本地证书编号与即将过期编号一致,需及时更换
console.warn("平台证书即将过期,请及时更换")
signature = resp_deprecated_sig
} else {
console.error("sn不匹配")
return false
}
const payload = `${url_path}\n${resp_appid}\n${resp_ts}\n${resp_data}`
const data_buffer = Buffer.from(payload, 'utf-8')
const sig_buffer = Buffer.from(signature, 'base64')
const key_obj = {
key: local_certificate,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST
}
const result = crypto.verify(
'RSA-SHA256',
data_buffer,
key_obj,
sig_buffer
)
return result
}
const ctx = getCtx()
const resp = getResp()
let res = checkSignature(ctx, resp)
console.log(res)
// RSAwithSHA256
import com.google.gson.JsonObject;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.security.Signature;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.MGF1ParameterSpec;
import java.security.spec.PSSParameterSpec;
import java.util.Base64;
public class RSA_Verify {
public static boolean checkSignature(JsonObject ctx,JsonObject resp){
String signature = null;
boolean result = false;
// 开发者本地信息
String local_appid = ctx.get("local_appid").getAsString();
String url_path = ctx.get("url_path").getAsString();
String local_sym_sn = ctx.get("local_sym_sn").getAsString();
String local_certificate = ctx.get("local_certificate").getAsString();
long respTs = resp.get("resp_ts").getAsLong();
String respAppid = resp.get("resp_appid").getAsString();
String respSn = resp.get("resp_sn").getAsString();
String respSig = resp.get("resp_sig").getAsString();
String respDeprecatedSn = resp.get("resp_deprecated_sn").getAsString();
String respDeprecatedSig = resp.get("resp_deprecated_sig").getAsString();
String respData = resp.get("resp_data").getAsString();
long localTs = System.currentTimeMillis() / 1000;
// 安全检查,根据业务实际需求判断
if (respAppid != local_appid || // 回包appid不正确
localTs - respTs > 300){ // 回包时间超过5分钟
System.out.println("安全字段校验失败");
return result;
}
if(local_sym_sn == respSn){
signature = respSig;
}else if(local_sym_sn == respDeprecatedSn){
System.out.println("平台证书即将过期,请及时更换"); // 本地证书编号与即将过期编号一致,需及时更换
signature = respDeprecatedSig;
}else{
System.out.println("sn不匹配");
return result;
}
String payload = url_path + "\n" + local_appid + "\n" + respTs + "\n" + respData;
byte[] dataBuffer = payload.getBytes(StandardCharsets.UTF_8);
try{
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
ByteArrayInputStream inputStream = new ByteArrayInputStream(local_certificate.getBytes());
X509Certificate certificate = (X509Certificate) certificateFactory.generateCertificate(inputStream);
Signature verifier = Signature.getInstance("RSASSA-PSS");
PSSParameterSpec pssParameterSpec = new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1);
verifier.setParameter(pssParameterSpec);
verifier.initVerify(certificate);
verifier.update(dataBuffer);
byte[] sig_buffer = Base64.getDecoder().decode(signature);
result = verifier.verify(sig_buffer);
}catch (Exception e){
e.printStackTrace();
}
return result;
}
private static JsonObject getCtx(){
JsonObject ctx = new JsonObject();
// 仅做演示,敏感信息请勿硬编码
String localCertificate = "-----BEGIN CERTIFICATE-----\n" +
"MIID0jCCArqgAwIBAgIUeE+Yy7vM/o+eHHsfM+1bGJJEZTQwDQYJKoZIhvcNAQEL\n" +
"BQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT\n" +
"FFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg\n" +
"Q0EwHhcNMjIwOTA1MDgzOTIyWhcNMjcwOTA0MDgzOTIyWjBkMRswGQYDVQQDDBJ3\n" +
"eGQ5MzBlYTVkNWEyNThmNGYxFTATBgNVBAoMDFRlbmNlbnQgSW5jLjEOMAwGA1UE\n" +
"CwwFV3hnTXAxCzAJBgNVBAYMAkNOMREwDwYDVQQHDAhTaGVuWmhlbjCCASIwDQYJ\n" +
"KoZIhvcNAQEBBQADggEPADCCAQoCggEBAM5D9qlkCmk1kr3FpF0e9pc3kGsvz5RA\n" +
"0/YRny9xPKIyV2UVMDZvRQ+mDHsiQQFE6etg457KFYSxTDKtItbdl6hJQVGeAvg0\n" +
"mqPYE9SkHRGTfL/AnXRbKBG2GC2OcaPSAprsLOersjay2me+9pF8VHybV8aox78A\n" +
"NsU75G/OO3V1iEE0s5Pmglqk8DEiw9gB/dGJzsNfXwzvyJyiUP9ZujYexyjsS+/Z\n" +
"GdSOUkqL/th+16yHj8alcdyga6YGfWEDyWkt/i/B28cwx4nzwk8xgrurifPaLuMk\n" +
"0+9wJQLCfAn/f7zyHrC8PcD1XvvRt9VBNMBASXs3710ODyyVf2lkMgkCAwEAAaOB\n" +
"gTB/MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgTwMGUGA1UdHwReMFwwWqBYoFaGVGh0\n" +
"dHA6Ly9ldmNhLml0cnVzLmNvbS5jbi9wdWJsaWMvaXRydXNjcmw/Q0E9MUJENDIy\n" +
"MEU1MERCQzA0QjA2QUQzOTc1NDk4NDZDMDFDM0U4RUJEMjANBgkqhkiG9w0BAQsF\n" +
"AAOCAQEAL2MK9tYu+ljLVBlSbfEeaKyF07TN+G31Ya5NBzeS1ZCx4joUEIyACWmG\n" +
"fUkKNKiKV+EMzxeEhKRso1Qif3E7Ipl+PQBoQw6OSR/jFHciYurnGR9CLkL03Zo1\n" +
"qw1Xetv9OipsvlpA0SOWc207e/XpGdm8C7FMXM6bzvVp8I/STTjC1vqjIZu9WavI\n" +
"RgGM4jyAPz2XogUq0BNijef8BXbbav9fAsXjHSwn5BQv4iLms3fiLm/eoyQ6dZ2R\n" +
"oTudrlcyr1bG4vwETLmHF+3yfVp9dpvJ+lyfiviwDwyfa8t2WlJm27DuF4vWoxir\n" +
"mjgj9tDutIFqxLIovLyg3uiAYtSQ/Q==\n" +
"-----END CERTIFICATE-----";
ctx.addProperty("local_certificate",localCertificate);
ctx.addProperty("local_sym_sn","79ba700ea147819f640941bceb38b1d1");
ctx.addProperty("local_appid","wxba6223c06417af7b");
ctx.addProperty("url_path","https://api.weixin.qq.com/wxa/getuserriskrank");
return ctx;
}
private static JsonObject getResp(){
JsonObject resp = new JsonObject();
resp.addProperty("resp_appid","wxba6223c06417af7b");
resp.addProperty("resp_ts",1635927956);
resp.addProperty("resp_sn","79ba700ea147819f640941bceb38b1d1");
resp.addProperty("resp_sig","Ht0VfQkkEweJ4hU266C14Aj64H9AXfkwNi5zxUZETCvR2svU1ZYdosDhFX/voLj1TyszqKsVxAlENGt7PPZZ8RQX7jnA4SKhiPUhW4LTbyTenisHJ+ohSfDjYnXavjQsBHspFS+BlPHuSSJ2xyQzw1+HuC6nid09ZL4FnGSYo4OI5MJrSb9xLzIVZMIDuUQchGKi/KaB1KzxECLEZcfjqbAgmxC7qOmuBLyO1WkHYDM95NJrHJWba5xv4wrwPru9yYTJSNRnlM+zrW5w9pOubC4Jtj3szTAEuOz9AcqUmgaAvMLNAIa8hfODLRe3n/cu4SgYlN/ZkNRU4QXVNbPGMg==");
resp.addProperty("resp_deprecated_sn","2171af9cdf1d7404423852e7e183d852");
resp.addProperty("resp_deprecated_sig","ZP1OODikAOePc+YJUMLxunF6xV05kextO/T1fy5lWv/CwV6OCsPBRM2xRRCi+B4lYXbbfYDdjzCz5BIAWEwIdjMlg/IHcJVHhRNAlKt5A3zvzfaJa5IJQel7xuUEXk/B6KVyEb41PbzrptjUGqWyTFMrjxQ4ThJfCuYocnUng7OuDU95enMqK2hZpO8o7kFW638BAwKDSiFNEwEJDWYkLz0kEw7ma3keezm4YHYKfJmjChK39tmZld7Rw/yrV1U9RiL/DO5ayP9VmrQkT/vYrPKyqI4/xKrIaTq44jFYTPIJKdU2OnLt6kjqwp2hvCzMuJdjRcrvzhWJ2A8xZ5hI2w==");
resp.addProperty("resp_data","{\"iv\":\"r2WDQt56rEAmMuoR\",\"data\":\"HExs66Ik3el+iM4IpeQ7SMEN934FRLFYOd3EmeaIrpP4EPTHckoco6O+PaoRZRa3lqaPRZT7r52f7LUok6gLxc6cdR8C4vpIIfh4xfLC4L7FNy9GbuMK1hcoi8b7gkWJcwZMkuCFNEDmqn3T49oWzAQOrY4LZnnnykv6oUJotdAsnKvmoJkLK7hRh7M2B1d2UnTnRuoIyarXc5Iojwoghx4BOvnV\",\"authtag\":\"z2BFD8QctKXTuBlhICGOjQ==\"}");
return resp;
}
public static void main(String[] args) {
JsonObject resp = getResp();
JsonObject ctx = getCtx();
boolean res = checkSignature(ctx,resp);
System.out.println(res);
}
}
本示例代码需要用到腾讯Kona国密套件:TencentKonaSMSuite
// SM2withSM3
import com.google.gson.JsonObject;
import com.tencent.kona.KonaProvider;
import com.tencent.kona.crypto.spec.SM2SignatureParameterSpec;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.security.Security;
import java.security.Signature;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.ECPublicKey;
import java.util.Base64;
public class SM2_Verify {
static{
Security.addProvider(new KonaProvider());
}
public static boolean checkSignature(JsonObject ctx, JsonObject resp){
String signature = null;
boolean result = false;
// 开发者本地信息
String local_appid = ctx.get("local_appid").getAsString();
String url_path = ctx.get("url_path").getAsString();
String local_sym_sn = ctx.get("local_sym_sn").getAsString();
String local_certificate = ctx.get("local_certificate").getAsString();
long respTs = resp.get("resp_ts").getAsLong();
String respAppid = resp.get("resp_appid").getAsString();
String respSn = resp.get("resp_sn").getAsString();
String respSig = resp.get("resp_sig").getAsString();
String respDeprecatedSn = resp.get("resp_deprecated_sn").getAsString();
String respDeprecatedSig = resp.get("resp_deprecated_sig").getAsString();
String respData = resp.get("resp_data").getAsString();
long localTs = System.currentTimeMillis() / 1000;
// 安全检查,根据业务实际需求判断
if (respAppid != local_appid || // 回包appid不正确
localTs - respTs > 300){ // 回包时间超过5分钟
System.out.println("安全字段校验失败");
return result;
}
if(local_sym_sn == respSn){
signature = respSig;
}else if(local_sym_sn == respDeprecatedSn){
System.out.println("平台证书即将过期,请及时更换"); // 本地证书编号与即将过期编号一致,需及时更换
signature = respDeprecatedSig;
}else{
System.out.println("sn不匹配");
return result;
}
String payload = url_path + "\n" + local_appid + "\n" + respTs + "\n" + respData;
byte[] dataBuffer = payload.getBytes(StandardCharsets.UTF_8);
try{
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509",KonaProvider.NAME);
ByteArrayInputStream inputStream = new ByteArrayInputStream(local_certificate.getBytes());
X509Certificate certificate = (X509Certificate) certificateFactory.generateCertificate(inputStream);
Signature verifier = Signature.getInstance("SM2");
SM2SignatureParameterSpec paramSpec = new SM2SignatureParameterSpec(local_sym_sn.getBytes(), (ECPublicKey) certificate.getPublicKey());
verifier.setParameter(paramSpec);
verifier.initVerify(certificate.getPublicKey());
verifier.update(dataBuffer);
byte[] sig_buffer = Base64.getDecoder().decode(signature);
result = verifier.verify(sig_buffer);
}catch (Exception e){
e.printStackTrace();
}
return result;
}
private static JsonObject getCtx(){
JsonObject ctx = new JsonObject();
// 仅做演示,敏感信息请勿硬编码
String localCertificate = "-----BEGIN CERTIFICATE-----\n" +
"MIICpTCCAkygAwIBAgIUaB2+dl2EhYIJt1KU3zYVk2Xb7+4wCgYIKoEcz1UBg3Uw\n" +
"gaUxCzAJBgNVBAYTAkNOMRIwEAYDVQQIDAlHdWFuZ0RvbmcxETAPBgNVBAcMCFNo\n" +
"ZW5aaGVuMRUwEwYDVQQKDAxUZW5jZW50IEluYy4xFjAUBgNVBAsMDVd4RGV2UGxh\n" +
"dGZvcm0xFjAUBgNVBAMMDVd4RGV2UGxhdGZvcm0xKDAmBgkqhkiG9w0BCQEWGVd4\n" +
"RGV2UGxhdGZvcm1AdGVuY2VudC5jb20wIhgPMjAyMjA5MDUxMjI0NTFaGA8yMDMy\n" +
"MDkwMjEyMjQ1MVowgZgxCzAJBgNVBAYTAkNOMRIwEAYDVQQIDAlHdWFuZ0Rvbmcx\n" +
"ETAPBgNVBAcMCFNoZW5aaGVuMRUwEwYDVQQKDAxUZW5jZW50IEluYy4xDjAMBgNV\n" +
"BAsMBVd4Z01wMRswGQYDVQQDDBJ3eDkzYTRjMDQ2MWJhNTg5YjQxHjAcBgkqhkiG\n" +
"9w0BCQEWD3dlaXhpbm1wQHFxLmNvbTBZMBMGByqGSM49AgEGCCqBHM9VAYItA0IA\n" +
"BCqW4Oxv3dEXgjs2mZNzt0lZIhaERohbJFxM3Nv4GKx70EDHIOYpo2ue9HEO8u28\n" +
"dXszQOG4xxDxbW4Y/If0SoqjYTBfMB8GA1UdIwQYMBaAFOZbGwNxANNz09qKnp4u\n" +
"iCDA9EJXMB0GA1UdDgQWBBThaf6MTqwNkDXulajs6lTR5Dkc2zAMBgNVHRMBAf8E\n" +
"AjAAMA8GA1UdDwEB/wQFAwMHwAAwCgYIKoEcz1UBg3UDRwAwRAIgOp0c64QSLUHx\n" +
"vbiPw/27dIcItvsN2F6m7VN41xebJx0CIHL0bp5okshjBF38XM07m4nWw55zAmmF\n" +
"EJc5Zq55kLC8\n" +
"-----END CERTIFICATE-----";
ctx.addProperty("local_certificate",localCertificate);
ctx.addProperty("local_sym_sn","8a98f6bba1415c0c4f6879bda6807861");
ctx.addProperty("local_appid","wxba6223c06417af7b");
ctx.addProperty("url_path","https://api.weixin.qq.com/wxa/getuserriskrank");
return ctx;
}
private static JsonObject getResp(){
JsonObject resp = new JsonObject();
resp.addProperty("resp_appid","wxba6223c06417af7b");
resp.addProperty("resp_ts",1692932963);
resp.addProperty("resp_sn","8a98f6bba1415c0c4f6879bda6807861");
resp.addProperty("resp_sig","MEUCIQC++rC5Zv+84JtUZx+2w/QzpIH3KyRsMCFPcSZlxD9gCQIgGfBRLUc0S13vSGmBPS5zyNfi1IEibxlaL0lPvV+Ap20=");
resp.addProperty("resp_deprecated_sn","1f2b56f9c26a60b33183eff4a3d9091a");
resp.addProperty("resp_deprecated_sig","ZP1OODikAOePc+YJUMLxunF6xV05kextO/T1fy5lWv/CwV6OCsPBRM2xRRCi+B4lYXbbfYDdjzCz5BIAWEwIdjMlg/IHcJVHhRNAlKt5A3zvzfaJa5IJQel7xuUEXk/B6KVyEb41PbzrptjUGqWyTFMrjxQ4ThJfCuYocnUng7OuDU95enMqK2hZpO8o7kFW638BAwKDSiFNEwEJDWYkLz0kEw7ma3keezm4YHYKfJmjChK39tmZld7Rw/yrV1U9RiL/DO5ayP9VmrQkT/vYrPKyqI4/xKrIaTq44jFYTPIJKdU2OnLt6kjqwp2hvCzMuJdjRcrvzhWJ2A8xZ5hI2w==");
resp.addProperty("resp_data","{\"iv\":\"b3sjwc9yvUEGH45l\",\"data\":\"fa8VugXI8UA2ugS646ZvuX0wo4qn0Eua2J9jtwACQXeVys3hP/fZDcZC4eEF9es/z/Zx6GM2piwoHKPmPbwzfNXWc/rUH/USFoKo6OBSiR8bb6QgkzYzYL9KsawMr8X/z6y8o3UzE5w65nfTySQFSpEVplD5S+SwQrLi3I2nUwS5N3SoJYsf8BHVfsYLBI9h1NocLgfjjyPYmeKsQ/t1muVWlV2Z75VbqFhM+ECgHpEvcWPDeUN5ZhZ6C/0=\",\"authtag\":\"cDZY4giOZgf73/CvObhypQ==\"}");
return resp;
}
public static void main(String[] args) {
JsonObject resp = getResp();
JsonObject ctx = getCtx();
boolean res = checkSignature(ctx,resp);
System.out.println(res);
}
}
# 解密
响应数据的加密算法、格式与请求时一致,可参考请求加密。
# 示例
以风控接口为例, 对原请求数据加密。
# AES256_GCM
密钥信息
{
"Sn": "fa05fe1e5bcc79b81ad5ad4b58acf787",
"Key": "otUpngOjU+nVQaWJIC3D/yMLV17RKaP6t4Ot9tbnzLY="
}
响应密文数据
{
"iv": "r2WDQt56rEAmMuoR",
"data": "HExs66Ik3el+iM4IpeQ7SMEN934FRLFYOd3EmeaIrpP4EPTHckoco6O+PaoRZRa3lqaPRZT7r52f7LUok6gLxc6cdR8C4vpIIfh4xfLC4L7FNy9GbuMK1hcoi8b7gkWJcwZMkuCFNEDmqn3T49oWzAQOrY4LZnnnykv6oUJotdAsnKvmoJkLK7hRh7M2B1d2UnTnRuoIyarXc5Iojwoghx4BOvnV",
"authtag": "z2BFD8QctKXTuBlhICGOjQ=="
}
解密后数据
{
"_n": "ShYZpqdVgY+yQVAxNSWhYg",
"_appid": "wxba6223c06417af7b",
"_timestamp": 1635927956,
"errcode": 0,
"errmsg": "getuserriskrank succ",
"risk_rank": 0,
"unoin_id": 2258658297
}
原响应数据
{
"errcode": 0,
"errmsg": "getuserriskrank succ",
"risk_rank": 0,
"unoin_id": 2258658297
}
示例代码
// AES256_GCM
const crypto = require("crypto")
// 仅做演示,敏感信息请勿硬编码
function getCtx() {
let ctx = {
local_sym_key: "otUpngOjU+nVQaWJIC3D/yMLV17RKaP6t4Ot9tbnzLY=",
local_sym_sn: "fa05fe1e5bcc79b81ad5ad4b58acf787",
local_appid: "wxba6223c06417af7b",
url_path: "https://api.weixin.qq.com/wxa/getuserriskrank"
}
return ctx
}
function getResp() {
let resp = {
resp_ts: 1635927956,
resp_data: '{"iv":"r2WDQt56rEAmMuoR","data":"HExs66Ik3el+iM4IpeQ7SMEN934FRLFYOd3EmeaIrpP4EPTHckoco6O+PaoRZRa3lqaPRZT7r52f7LUok6gLxc6cdR8C4vpIIfh4xfLC4L7FNy9GbuMK1hcoi8b7gkWJcwZMkuCFNEDmqn3T49oWzAQOrY4LZnnnykv6oUJotdAsnKvmoJkLK7hRh7M2B1d2UnTnRuoIyarXc5Iojwoghx4BOvnV","authtag":"z2BFD8QctKXTuBlhICGOjQ=="}'
}
return resp
}
function getRealResp(ctx, resp) {
const { local_sym_key, local_sym_sn, local_appid, url_path } = ctx // 开发者本地信息
const { resp_ts, resp_data } = resp // API响应数据,解密只需要响应头时间戳与响应数据
const { iv, data, authtag } = JSON.parse(resp_data)
const aad = `${url_path}|${local_appid}|${resp_ts}|${local_sym_sn}`
const real_aad = Buffer.from(aad, "utf-8")
const real_key = Buffer.from(local_sym_key, "base64")
const real_iv = Buffer.from(iv, "base64")
const real_data = Buffer.from(data, "base64")
const real_authtag = Buffer.from(authtag, "base64")
const decipher = crypto.createDecipheriv("aes-256-gcm", real_key, real_iv)
decipher.setAAD(real_aad)
decipher.setAuthTag(real_authtag)
let decipher_update = decipher.update(real_data)
let decipher_final
try {
decipher_final = decipher.final()
} catch (error) {
console.error("auth tag验证失败")
return {}
}
const real_deciphertext = Buffer.concat([decipher_update, decipher_final])
const deciphertext = real_deciphertext.toString("utf-8")
const real_resp = JSON.parse(deciphertext)
const local_ts = Math.floor(Date.now() / 1000)
if (
// 安全检查,根据业务实际需求判断
real_resp["_appid"] != local_appid || // appid不匹配
real_resp["_timestamp"] != resp_ts || // timestamp与Wechatmp-TimeStamp不匹配
local_ts - real_resp["_timestamp"] > 300 // 响应数据的时候与当前时间超过5分钟
) {
console.error("安全字段校验失败")
return {}
}
return real_resp
}
const ctx = getCtx()
const resp = getResp()
let res = getRealResp(ctx, resp)
console.log(res)
// AES256_GCM
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
public class AES_Dec {
public static Object getRealResp(JsonObject ctx,JsonObject resp){
byte[] decryptedBytes = null;
// 开发者本地信息
String local_appid = ctx.get("local_appid").getAsString();
String url_path = ctx.get("url_path").getAsString();
String local_sym_sn = ctx.get("local_sym_sn").getAsString();
String local_sym_key = ctx.get("local_sym_key").getAsString();
// API响应数据,解密只需要响应头时间戳与响应数据
long respTs = resp.get("resp_ts").getAsLong();
String respData = resp.get("resp_data").getAsString();
JsonParser parser = new JsonParser();
JsonElement resp_data = parser.parse(respData);
String iv = resp_data.getAsJsonObject().get("iv").getAsString();
String data = resp_data.getAsJsonObject().get("data").getAsString();
String authtag = resp_data.getAsJsonObject().get("authtag").getAsString();
// 构建AAD
String aad = url_path + "|" + local_appid + "|" + respTs + "|" + local_sym_sn;
// 拼接cipher和authtag
byte[] dataBytes = Base64.getDecoder().decode(data);
byte[] authtagBytes = Base64.getDecoder().decode(authtag);
byte[] new_dataBytes = new byte[dataBytes.length+authtagBytes.length];
System.arraycopy(dataBytes,0,new_dataBytes,0,dataBytes.length);
System.arraycopy(authtagBytes,0,new_dataBytes,dataBytes.length,authtagBytes.length);
byte[] aadBytes = aad.getBytes(StandardCharsets.UTF_8);
byte[] ivBytes = Base64.getDecoder().decode(iv);
Object realResp = null;
try
{
byte[] keyBytes = Base64.getDecoder().decode(local_sym_key);
SecretKeySpec secretKey = new SecretKeySpec(keyBytes, "AES");
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, ivBytes);
cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec);
cipher.updateAAD(aadBytes);
try{
decryptedBytes = cipher.doFinal(new_dataBytes);
}catch (Exception e){
System.out.println("auth tag验证失败");
return null;
}
// 解密结果
String decryptedData = new String(decryptedBytes, StandardCharsets.UTF_8);
JsonElement element = parser.parse(decryptedData);
Gson gson = new Gson();
realResp = gson.fromJson(element,Object.class);
long localTs = System.currentTimeMillis() / 1000;
// 安全检查,根据业务实际需求判断
if (element.getAsJsonObject().get("_appid").getAsString() == local_appid // appid不匹配
|| element.getAsJsonObject().get("_timestamp").getAsLong() != respTs // timestamp与Wechatmp-TimeStamp不匹配
|| localTs - element.getAsJsonObject().get("_timestamp").getAsLong() > 300 // 响应数据的时候与当前时间超过5分钟
) {
System.out.println("安全字段校验失败");
return null;
}
}catch(Exception e){
e.printStackTrace();
}
return realResp;
}
private static JsonObject getCtx(){
JsonObject ctx = new JsonObject();
// 仅做演示,敏感信息请勿硬编码
ctx.addProperty("local_sym_key","otUpngOjU+nVQaWJIC3D/yMLV17RKaP6t4Ot9tbnzLY=");
ctx.addProperty("local_sym_sn","fa05fe1e5bcc79b81ad5ad4b58acf787");
ctx.addProperty("local_appid","wxba6223c06417af7b");
ctx.addProperty("url_path","https://api.weixin.qq.com/wxa/getuserriskrank");
return ctx;
}
private static JsonObject getResp(){
JsonObject resp = new JsonObject();
String respData = "{\"iv\":\"r2WDQt56rEAmMuoR\",\"data\":\"HExs66Ik3el+iM4IpeQ7SMEN934FRLFYOd3EmeaIrpP4EPTHckoco6O+PaoRZRa3lqaPRZT7r52f7LUok6gLxc6cdR8C4vpIIfh4xfLC4L7FNy9GbuMK1hcoi8b7gkWJcwZMkuCFNEDmqn3T49oWzAQOrY4LZnnnykv6oUJotdAsnKvmoJkLK7hRh7M2B1d2UnTnRuoIyarXc5Iojwoghx4BOvnV\",\"authtag\":\"z2BFD8QctKXTuBlhICGOjQ==\"}";
resp.addProperty("resp_ts",1635927956);
resp.addProperty("resp_data",respData);
return resp;
}
public static void main(String[] args) {
JsonObject resp = getResp();
JsonObject ctx = getCtx();
Object res = getRealResp(ctx,resp);
System.out.println(res);
}
}
本示例代码需要用到腾讯Kona国密套件:TencentKonaSMSuite
// SM4_GCM
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.tencent.kona.KonaProvider;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.Security;
import java.util.Base64;
public class SM4_Dec {
static {
Security.addProvider(new KonaProvider());
}
public static Object getRealResp(JsonObject ctx, JsonObject resp){
byte[] decryptedBytes = null;
// 开发者本地信息
String local_appid = ctx.get("local_appid").getAsString();
String url_path = ctx.get("url_path").getAsString();
String local_sym_sn = ctx.get("local_sym_sn").getAsString();
String local_sym_key = ctx.get("local_sym_key").getAsString();
// API响应数据,解密只需要响应头时间戳与响应数据
long respTs = resp.get("resp_ts").getAsLong();
String respData = resp.get("resp_data").getAsString();
JsonParser parser = new JsonParser();
JsonElement resp_data = parser.parse(respData);
String iv = resp_data.getAsJsonObject().get("iv").getAsString();
String data = resp_data.getAsJsonObject().get("data").getAsString();
String authtag = resp_data.getAsJsonObject().get("authtag").getAsString();
// 构建AAD
String aad = url_path + "|" + local_appid + "|" + respTs + "|" + local_sym_sn;
// 拼接cipher和authtag
byte[] dataBytes = Base64.getDecoder().decode(data);
byte[] authtagBytes = Base64.getDecoder().decode(authtag);
byte[] new_dataBytes = new byte[dataBytes.length+authtagBytes.length];
System.arraycopy(dataBytes,0,new_dataBytes,0,dataBytes.length);
System.arraycopy(authtagBytes,0,new_dataBytes,dataBytes.length,authtagBytes.length);
byte[] aadBytes = aad.getBytes(StandardCharsets.UTF_8);
byte[] ivBytes = Base64.getDecoder().decode(iv);
Object realResp = null;
try
{
byte[] keyBytes = Base64.getDecoder().decode(local_sym_key);
SecretKeySpec secretKey = new SecretKeySpec(keyBytes, "SM4");
Cipher cipher = Cipher.getInstance("SM4/GCM/NoPadding");
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, ivBytes);
cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec);
cipher.updateAAD(aadBytes);
try{
decryptedBytes = cipher.doFinal(new_dataBytes);
}catch (Exception e){
System.out.println("auth tag验证失败");
return null;
}
// 解密结果
String decryptedData = new String(decryptedBytes, StandardCharsets.UTF_8);
JsonElement element = parser.parse(decryptedData);
Gson gson = new Gson();
realResp = gson.fromJson(element,Object.class);
long localTs = System.currentTimeMillis() / 1000;
// 安全检查,根据业务实际需求判断
if (element.getAsJsonObject().get("_appid").getAsString() == local_appid // appid不匹配
|| element.getAsJsonObject().get("_timestamp").getAsLong() != respTs // timestamp与Wechatmp-TimeStamp不匹配
|| localTs - element.getAsJsonObject().get("_timestamp").getAsLong() > 300 // 响应数据的时候与当前时间超过5分钟
) {
System.out.println("安全字段校验失败");
return null;
}
}catch(Exception e){
e.printStackTrace();
}
return realResp;
}
private static JsonObject getCtx(){
JsonObject ctx = new JsonObject();
// 仅做演示,敏感信息请勿硬编码
ctx.addProperty("local_sym_key","YXBwaWRkdXNlZm9ydGVzdA==");
ctx.addProperty("local_sym_sn","fa05fe1e5bcc79b81ad5ad4b58acf787");
ctx.addProperty("local_appid","wxba6223c06417af7b");
ctx.addProperty("url_path","https://api.weixin.qq.com/wxa/getuserriskrank");
return ctx;
}
private static JsonObject getResp(){
JsonObject resp = new JsonObject();
String respData = "{\"iv\":\"b3sjwc9yvUEGH45l\",\"data\":\"fa8VugXI8UA2ugS646ZvuX0wo4qn0Eua2J9jtwACQXeVys3hP/fZDcZC4eEF9es/z/Zx6GM2piwoHKPmPbwzfNXWc/rUH/USFoKo6OBSiR8bb6QgkzYzYL9KsawMr8X/z6y8o3UzE5w65nfTySQFSpEVplD5S+SwQrLi3I2nUwS5N3SoJYsf8BHVfsYLBI9h1NocLgfjjyPYmeKsQ/t1muVWlV2Z75VbqFhM+ECgHpEvcWPDeUN5ZhZ6C/0=\",\"authtag\":\"cDZY4giOZgf73/CvObhypQ==\"}";
resp.addProperty("resp_ts",1692932963);
resp.addProperty("resp_data",respData);
return resp;
}
public static void main(String[] args) {
JsonObject resp = getResp();
JsonObject ctx = getCtx();
Object res = getRealResp(ctx,resp);
System.out.println(res);
}
}
# 错误码
错误码 | 错误码取值 | 解决方案 |
---|---|---|
40230 | API_Missing_Wechatmp_Serial | 缺少Wechatmp_Serial |
40231 | API_Missing_Wechatmp_Timestamp | 缺少Wechatmp_Timestamp |
40232 | API_Missing_Wechatmp_Signature | 缺少Wechatmp_Signature |
40233 | API_Missing_Wechatmp_Appid | 缺少Wechatmp_Appid |
40234 | API_Invalid_Signature | 签名错误 |
40235 | API_Invalid_Encrypt | 错误的加密 |
40236 | API_Invalid_Wechatmp_Appid | 无效的Wechatmp_Appid |
40237 | API_Invalid_Wechatmp_Appidmatch | Wechatmp_Appid和Token不匹配 |
40238 | API_NoExist_DevSecretSym | 开发者未设置对称密钥 |
40239 | API_NoExist_DevSecretAsym | 开发者未设置公钥 |
40240 | API_Expired_Wechatmp_Timestamp | 超时的数据 |