# 概述

在小程序管理后台开启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)

# 签名

开发者需要对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)

# 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)

# 解密

响应数据的加密算法、格式与请求时一致,可参考请求加密。

# 示例

风控接口为例, 对原请求数据加密。

# 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)

# 错误码

错误码 错误码取值 解决方案
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 超时的数据