# 小程序分享签名
# 概述
为保障微信自定义分享功能链路安全,我们提供了分享安全校验功能。开启分享安全校验选项后,小程序的分享接口需要带上开发者签名字段,平台会校验分享数据的签名,签名验证失败的分享请求会被降级。
# 注意事项
开启验签功能后,自定义分享必须指定imageUrl,如果不传入会直接展示降级卡片,不会展示用户当前截图与写入的title
小程序降级体验:默认分享标题及默认分享图,具体降级体验如下图所示
开启分享安全校验前,需先在平台配置开发者公钥或开发者证书。
小程序配置流程可参考安全鉴权模式介绍
# 签名算法
开发者在配置公钥或证书时会指定签名算法。平台支持如下两种签名算法,
- RSAwithSHA256
- SM2withSM3
# 签名生成
签名串生成规则 Key1=Base64(Value1)&Key2=Base64(Value2)...
其中Key为各待签名字段,Value为待签名数据。Key保留大小写,所有字段按Key的字典序排序(根据ASCII码值从小到大排序)。Value需进行标准Base64编码(无换行符\n)。若Value内包含Unicode字符,需先进行UTF-8编码。
签名串生成后,使用配置的签名算法进行签名。
若算法为RSAwithSHA256,需使用PSS填充方式,指定salt_length为32。(PSS签名中包含随机因子,因此每次签名结果都不同,只能验签)
# 分享接口字段
参考小程序开发手册onShareAppMessage接口。
原分享接口需要传入title、path、imageUrl三个字段,其中imageUrl下载得到的图片,需计算其SHA256哈希值thumbDataHash。
分享对象中新增了图片哈希thumbDataHash与签名signature两个字段,开发者需先计算imageUrl对应图片的哈希值。
开发者需要对appid、path、thumbDataHash、title四个字段进行签名。生成签名后,填入分享对象的 signature 字段。
# 签名字段
| 字段名 | 说明 | 备注 |
|---|---|---|
| appid | 当前appid | |
| path | 待分享路径 | |
| thumbDataHash | 图片SHA256 | 新增字段,用小写十六进制串表示 |
| title | 待分享标题 |
# 示例
# RSAwithSHA256
签名使用PSS填充方式,需要指定salt长度为32。(PSS签名中包含随机因子,因此每次签名结果都会变化)
私钥信息
{
"Appid": "wxba6223c06417af7b",
"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-----",
"PublicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3FoQOmOl5/CF5hF7ta4E\nzCy2LaU3Eu2k9DBwQ73J82I53Sx9LAgM1DH3IsYohRRx/BESfbdDI2powvr6QYKV\nIC+4Yavwg7gzhZRxWWmT1HruEADCZAgkUCu+9Il/9FPuitPSoIpBd07NqdkkRe82\nNBOfrKTdhge/5zd457fl7J81Q5VTIxO8vvq7FSw7k6Jtv+eOjR6SZOWbbUO7f9r4\nUuUkXmvdGv21qiqtaO1EMw4tUCELzY73M7NpCH3RorlommYX3P6q0VrkDHrCE0/Q\nMhmHsF+46E+IRcJ3wtEj3p/mO1VoCpEhawC1U728ZUTwWNEii8hPEhcNAZTKaQMa\nTQIDAQAB\n-----END PUBLIC KEY-----"
}
原始分享数据
{
"title": "WXA分享测试",
"path": "/pages/index/share?input=%E6%B5%8B%E8%AF%95",
"imageUrl": "http://mmbiz.qpic.cn/mmbiz_png/UB2CE27DppnHN915Br0w03KEj4IQWatAncjLibpBy447Sl2rWb4J2sTQjIXg54DDApGIjDyPppAxe0E3MmCmficA/0?wx_fmt=png"
}
计算缩略图哈希值
下载imageUrl对应的图片,并计算其SHA256哈希值thumbDataHash
thumbDataHash = 12c593c5bd2f0972188eb30e32bee7ad4060be582c2c6826caff89372ca84707
拼接签名串
按字典序对应签名字段排序
appid
path
thumbDataHash
title
对字段内容进行标准base64编码,若存在中文等Unicode字符,需先进行UTF-8编码
appid = d3hiYTYyMjNjMDY0MTdhZjdi // wxba6223c06417af7b
path = L3BhZ2VzL2luZGV4L3NoYXJlP2lucHV0PSVFNiVCNSU4QiVFOCVBRiU5NQ== // /pages/index/share?input=%E6%B5%8B%E8%AF%95
thumbDataHash = MTJjNTkzYzViZDJmMDk3MjE4OGViMzBlMzJiZWU3YWQ0MDYwYmU1ODJjMmM2ODI2Y2FmZjg5MzcyY2E4NDcwNw== // 12c593c5bd2f0972188eb30e32bee7ad4060be582c2c6826caff89372ca84707
title = V1hB5YiG5Lqr5rWL6K+V // WXA分享测试
拼接各个字段(末尾无换行符,SHA256为242c3d1ed8e95fcd418cfb3257c23773fb8d698cd35cb0998567543348090d5c)
appid=d3hiYTYyMjNjMDY0MTdhZjdi&path=L3BhZ2VzL2luZGV4L3NoYXJlP2lucHV0PSVFNiVCNSU4QiVFOCVBRiU5NQ==&thumbDataHash=MTJjNTkzYzViZDJmMDk3MjE4OGViMzBlMzJiZWU3YWQ0MDYwYmU1ODJjMmM2ODI2Y2FmZjg5MzcyY2E4NDcwNw==&title=V1hB5YiG5Lqr5rWL6K+V
计算签名
使用PSS填充方式计算签名S(内含随机因子每次结果都不同,需使用公钥验证签名)
base64_encode(S) = 2+gb12KQTNULmZM8J/PsTex0Y532LcCh3U9m2pZBPOp91VqjH9yyWh9LKJ7IbTbRguT2lkxWQvkjEAwa+6WJAOkHFeSUAZwieG47twQba6b4Y1p3Z0Eiaa1NnfsjEYWOkZY/Yu5+u5XpwYAWWy1cQrYNppSLqQ5rhZIndQIRt0rpA65kqcLg9ENMI16X9JvwmtUjKEglf0k2JrCtEu4+7un78QnYApazxcHAP6LYPcahgD8lrYyO4e/nFle9Toe2/ZzB2wE+Aac1DOoZ+Taj85LVNT/9PazbD8z0OucJJLRjpw0zMWezJHuspqU50RT4zV5z7DihUnH/KvklD9cdxg==
最终分享对象
{
"title": "WXA分享测试",
"path": "/pages/index/share?input=%E6%B5%8B%E8%AF%95",
"imageUrl": "http://mmbiz.qpic.cn/mmbiz_png/UB2CE27DppnHN915Br0w03KEj4IQWatAncjLibpBy447Sl2rWb4J2sTQjIXg54DDApGIjDyPppAxe0E3MmCmficA/0?wx_fmt=png",
"thumbDataHash": "12c593c5bd2f0972188eb30e32bee7ad4060be582c2c6826caff89372ca84707",
"signature": "2+gb12KQTNULmZM8J/PsTex0Y532LcCh3U9m2pZBPOp91VqjH9yyWh9LKJ7IbTbRguT2lkxWQvkjEAwa+6WJAOkHFeSUAZwieG47twQba6b4Y1p3Z0Eiaa1NnfsjEYWOkZY/Yu5+u5XpwYAWWy1cQrYNppSLqQ5rhZIndQIRt0rpA65kqcLg9ENMI16X9JvwmtUjKEglf0k2JrCtEu4+7un78QnYApazxcHAP6LYPcahgD8lrYyO4e/nFle9Toe2/ZzB2wE+Aac1DOoZ+Taj85LVNT/9PazbD8z0OucJJLRjpw0zMWezJHuspqU50RT4zV5z7DihUnH/KvklD9cdxg=="
}
示例代码
// 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"
}
return ctx
}
function getReq() {
let req = {
appid: "wxba6223c06417af7b",
title: "WXA分享测试",
path: "/pages/index/share?input=%E6%B5%8B%E8%AF%95",
thumbDataHash: "12c593c5bd2f0972188eb30e32bee7ad4060be582c2c6826caff89372ca84707"
}
return req
}
function getShareParams() {
let param_list = ["appid", "title", "path", "thumbDataHash"]
return param_list
}
function getSignature(ctx, req) {
const { local_private_key } = ctx // 开发者本地信息
const param_list = getShareParams()
param_list.sort() // 确保参数列表按字典序排序
let payload = ""
for (let i = 0; i < param_list.length; i += 1) {
const param = param_list[i]
let value = req[param]
if (typeof value !== "string") value = ""
let new_value = Buffer.from(value, "utf-8").toString("base64")
if (i > 0) payload += "&"
payload += `${param}=${new_value}`
}
// console.log(payload)
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
}
const ctx = getCtx()
const req = getReq()
let res = getSignature(ctx, req)
console.log(res)