微信公众号自定义菜单支持配置跳转小程序,开发者可以在 pagepath 字段里带上页面路径和查询参数。比如 pages/order/detail?id=10086&from=menu 这种形式,参数会原样带到小程序启动页面。因为菜单配置在微信公众平台后台或通过接口下发,参数内容本身没有额外的保护,只要有人拿到小程序卡片或者抓包菜单接口,就能直接看到这些字段。对于订单号、用户标识、场景码这类敏感信息来说,这种明文传递方式存在泄露和篡改风险。解决办法之一就是在服务端先把敏感参数用 RSA 公钥加密,再把密文拼接到 pagepath,小程序端拿到参数后用私钥解密还原。

一、自定义菜单跳转小程序的参数传递机制与风险
微信自定义菜单跳转小程序时,菜单结构里有一个 pagepath 字段,用来指定要打开的小程序页面。这个字段除了路径之外,还可以携带查询参数。例如 pages/user/profile?uid=10001&scene=menu,小程序在 onLoad 生命周期里可以直接从 options 里取到 uid 和 scene。官方文档对 pagepath 长度有限制,通常不能超过 1024 字节,路径过长会导致菜单创建失败。
问题在于,这些参数是明文存放在菜单配置里的。用户分享小程序卡片时,如果卡片携带了当时的 pagepath,接收方同样可以看到参数内容。更麻烦的是,如果参数里有用户手机号、订单号、会员等级等信息,一旦被转发到群里或者截图传播,就会造成敏感数据泄漏。另外,攻击者如果了解参数结构,可以尝试修改 uid 等字段,模拟别的用户访问页面,造成越权查询或数据串号。
还有一类场景是服务端通过接口动态创建菜单,参数可能来自数据库或用户上下文。开发人员有时为了省事,直接把原始字段拼接进去,忽略了日志记录和第三方监控也可能把这些明文参数采集走。综合来看,把敏感参数做一次加密再传输,不仅能降低泄露风险,也能提高参数被篡改的门槛。
二、RSA 加密方案设计与密钥管理
RSA 是一种非对称加密算法,它有一对密钥:公钥和私钥。公钥加密的数据只能用对应的私钥解密,私钥加密的数据则可以用公钥验证。在微信公众号菜单跳转小程序的场景里,我们需要服务端加密、小程序端解密,所以加密方应该持有公钥,解密方持有私钥。这意味着私钥不能放在服务端,而应该由小程序端生成并妥善保存。
常见的做法是:小程序首次启动时生成一对 RSA 密钥,把私钥存储在本地缓存或安全位置,把公钥上传到业务服务器。服务端后续配置菜单时,把需要传递的敏感参数组装成 JSON,使用公钥加密,再对密文做 Base64 和安全 URL 编码,最后拼接到 pagepath。小程序收到参数后,先做 URL 解码,取出密文,再用本地私钥解密并还原 JSON。这样即使密文被截获,没有私钥也无法得到明文。
密钥管理有几个要点需要注意。第一,私钥绝对不能写死在小程序代码包里,也不要通过接口下发,否则反编译后很容易被提取。第二,RSA 加密对数据长度有限制,以 2048 位密钥为例,PKCS#1 v1.5 填充最多只能加密 245 字节,OAEP 填充更少一些。因此敏感参数应当是精简后的少量字段,不要试图加密大段 JSON。第三,密文经过 Base64 后可能包含 +、/、= 等字符,直接放在 URL 里会产生歧义,需要做一次 encodeURIComponent 或者改用 Base64URL 字符集。
如果参数确实比较长,可以考虑混合加密方案:用 AES 加密实际参数,再用 RSA 加密 AES 密钥。不过对菜单跳转这种场景,通常只需要隐藏用户标识、订单号等短参数,直接使用 RSA 加密已经足够。
三、代码实现:服务端加密与小程序端解密
服务端以 Node.js 为例,使用内置 crypto 模块即可完成 RSA 公钥加密。下面代码先构造参数对象,再使用 OAEP 填充方式加密,最后输出适合拼接 pagepath 的密文。公钥需要提前从小程序端获取。
const crypto = require('crypto');
function encryptParams(paramsObj, publicKey) {
const json = JSON.stringify(paramsObj);
const buffer = Buffer.from(json, 'utf8');
const encrypted = crypto.publicEncrypt(
{
key: publicKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
},
buffer
);
// 使用 base64url 避免出现 + / = 等 URL 敏感字符
return encrypted.toString('base64url');
}
// 实际公钥需要从小程序端上传获得,这里仅为结构示例
const publicKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----`;
const params = {
userId: '18888888888',
orderNo: 'A202401011234',
scene: 'menu_order_detail'
};
const encrypted = encryptParams(params, publicKey);
const pagepath = 'pages/order/detail?payload=' + encodeURIComponent(encrypted);
console.log(pagepath);
小程序端需要引入支持 RSA 解密的库,常见做法是基于 jsencrypt 或自行封装纯 JS 实现。下面示例展示在页面 onLoad 中取出 payload 参数,从本地存储获取私钥后解密。实际开发时请确认库对小程序的兼容性,必要时使用适配版本。
import JSEncrypt from 'jsencrypt';
Page({
onLoad(options) {
const payload = options.payload;
if (!payload) {
console.error('缺少加密参数');
return;
}
const privateKey = wx.getStorageSync('rsa_private_key');
const decryptor = new JSEncrypt();
decryptor.setPrivateKey(privateKey);
const decrypted = decryptor.decrypt(decodeURIComponent(payload));
if (decrypted) {
const params = JSON.parse(decrypted);
this.setData({
orderNo: params.orderNo,
userId: params.userId,
scene: params.scene
});
} else {
console.error('解密失败,可能是私钥不匹配或密文被篡改');
}
}
});
如果希望后端不依赖 Node.js,也可以使用 Java、Python 等语言实现。原理相同,只要保证填充方式和字符集处理一致即可。Java 端可用 Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"),Python 端可用 cryptography 库的 OAEP 填充。关键点是加密结果统一转成 Base64URL 再做 URL 编码,避免参数在微信侧被截断或转义。
四、上线前的注意事项与常见问题
首先是参数长度控制。加密后的密文比明文长很多,2048 位 RSA 加密 100 字节明文,密文通常在 256 字节左右,再经过 Base64 和 URL 编码会进一步膨胀。菜单 pagepath 有长度限制,因此要尽量精简字段,不要把所有业务数据都塞进去。例如只传 userId 和 orderNo,其他信息小程序进入页面后再通过接口获取。
其次是密钥更新机制。如果小程序端私钥丢失或者怀疑泄漏,需要重新生成密钥对并上传公钥。服务端应当支持公钥版本管理,在菜单参数里可以带上 keyVersion 字段,方便小程序端选择对应私钥解密。不要频繁更换密钥,否则已经下发的菜单可能因为私钥不匹配而无法打开。
最后是错误处理。解密失败时,小程序应当给出友好提示,而不是直接展示原始密文或崩溃。可以在 onLoad 中做 try-catch,失败后跳转到错误页或引导用户重新进入。服务端记录菜单创建日志时,不要记录加密前的明文参数,避免日志本身成为泄漏源。结合登录态和接口权限校验,才能形成完整的敏感参数保护链路。