# 小程序分享签名

# 概述

为保障微信自定义分享功能链路安全,我们提供了分享安全校验功能。开启分享安全校验选项后,小程序的分享接口需要带上开发者签名字段,平台会校验分享数据的签名,签名验证失败的分享请求会被降级。

# 注意事项

开启验签功能后,自定义分享必须指定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接口。

原分享接口需要传入titlepathimageUrl三个字段,其中imageUrl下载得到的图片,需计算其SHA256哈希值thumbDataHash

分享对象中新增了图片哈希thumbDataHash与签名signature两个字段,开发者需先计算imageUrl对应图片的哈希值。

开发者需要对appidpaththumbDataHashtitle四个字段进行签名。生成签名后,填入分享对象的 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)