服务端api签名指南
概述
在小程序管理后台开启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==
示例代码
nodejs java java_sm
// 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)
签名
开发者需要对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 java java_sm
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)
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==
示例代码
nodejs java java_sm
// 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)
解密
响应数据的加密算法、格式与请求时一致,可参考请求加密。
示例
以风控接口为例, 对原请求数据加密。
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
}
示例代码
nodejs java java_sm
// 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)
错误码
错误码 | 错误码取值 | 解决方案 |
---|---|---|
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 | 超时的数据 |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY