微信分享
https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html
Nodejs express 服务端
开发必要
node 版本需要大于等 18.x
node -v
v18.16.0
1. 获取公众号 AppID 和 AppSecret
获取 AppID 和 AppSecret 步骤:公众号
- 左侧菜单 设置与开发
- 基本信息
- 公众号开发信息
创建 config.json
Windows
ni config.json
Mac/Linux 终端
touch config.json
文件内容如下,下面 AppID、AppSecret 需改成你自己的
{
"AppID": "wxe4347189d0252cb8",
"AppSecret": "0683e7ccb7a9c0ee015fa17dd13f5b2f"
}
2. 获取 access_token 和 jsapi_ticket
获取 access_token
- 官方文档 附录 1-JS-SDK 使用权限签名算法:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#62
- 官方文档 https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html
为了实现获取 jsapi_ticket
并使用文件系统进行缓存的功能,我们可以创建一个新的函数来处理这个过程。这个函数将使用由 fetchAccessToken
提供的 access_token
来请求 jsapi_ticket
,然后将其存储在一个文件中,并检查是否有效,类似于我们之前对 access_token
的处理。
下面是完整的代码实现,包括了 fetchJsapiTicket
函数,这个函数将依赖于 fetchAccessToken
函数来先获取有效的 access_token
:
创建 helper.js Windows powershell
ni helper.js
Mac/Linux 终端
touch helper.js
helper.js 内容如下
const fs = require("fs").promises;
const config = require("./config.json");
const tokenPath = "./token.json"; // Token 存储路径
const ticketPath = "./ticket.json"; // Ticket 存储路径
let fetchPromise = null; // 全局 promise,确保只有一个 token 请求在进行
let fetchTicketPromise = null; // 全局 promise,确保只有一个 ticket 请求在进行
async function fetchAccessToken() {
if (fetchPromise) {
return fetchPromise;
}
fetchPromise = (async () => {
try {
const currentTime = Math.floor(Date.now() / 1000);
if (await checkTokenFile(tokenPath, currentTime)) {
const tokenData = await fs.readFile(tokenPath, "utf8");
const { access_token } = JSON.parse(tokenData);
console.log("Using cached token:", access_token);
return access_token;
}
const response = await fetch(
`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${config.AppID}&secret=${config.AppSecret}`
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log("[fetchAccessToken data]", data);
if (data.errcode) {
throw data;
}
const newTokenData = JSON.stringify({
access_token: data.access_token,
expires_at: currentTime + data.expires_in,
});
await fs.writeFile(tokenPath, newTokenData);
console.log("New token fetched and cached:", data.access_token);
return data.access_token;
} catch (error) {
console.error("error There was a problem fetching the data:", error);
throw error;
} finally {
fetchPromise = null;
}
})();
return fetchPromise;
}
async function fetchJsapiTicket() {
if (fetchTicketPromise) {
return fetchTicketPromise;
}
fetchTicketPromise = (async () => {
try {
const currentTime = Math.floor(Date.now() / 1000);
if (await checkTokenFile(ticketPath, currentTime)) {
const ticketData = await fs.readFile(ticketPath, "utf8");
const { ticket } = JSON.parse(ticketData);
console.log("Using cached ticket:", ticket);
return ticket;
}
const access_token = await fetchAccessToken();
const response = await fetch(
`https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${access_token}&type=jsapi`
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log("[fetchJsapiTicket data]", data);
if (data.errcode) {
throw data;
}
const newTicketData = JSON.stringify({
ticket: data.ticket,
expires_at: currentTime + data.expires_in,
});
await fs.writeFile(ticketPath, newTicketData);
console.log("New jsapi ticket fetched and cached:", data.ticket);
return data.ticket;
} catch (error) {
console.error("There was a problem fetching the jsapi ticket:", error);
throw error;
} finally {
fetchTicketPromise = null;
}
})();
return fetchTicketPromise;
}
// Function to check the validity of token or ticket
async function checkTokenFile(filePath, currentTime) {
try {
const fileData = await fs.readFile(filePath, "utf8");
const { expires_at } = JSON.parse(fileData);
return currentTime < expires_at;
} catch (error) {
return false;
}
}
// Testing the function
// fetchJsapiTicket().then((ticket) => console.log("Jsapi Ticket:", ticket));
module.exports = {
fetchAccessToken,
fetchJsapiTicket,
checkTokenFile,
};
代码说明:
- fetchJsapiTicket:此函数确保获取
jsapi_ticket
时首先有一个有效的access_token
。它将检查本地文件是否有有效的jsapi_ticket
,如果没有或已过期,则使用有效的access_token
从微信 API 获取新的jsapi_ticket
并缓存它。 - 全局变量:
fetchTicketPromise
类似于之前的fetchPromise
,确保在任何时候只有一个jsapi_ticket
请求正在进行。 - checkTokenFile:通用的函数来检查 token 或 ticket 文件的有效性。
这种设计保证了即使在并发请求的场景下,系统的行为也是正确的,不会导致不必要的 API 请求。
3. 获取 sign 签名代码
官方文档 附录 1-JS-SDK 使用权限签名算法:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#62
官方文档
附录6-DEMO页面和示例代码
:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#67
下载 sample 并解压出来
点击下载:https://www.weixinsxy.com/jssdk/sample.zip
修改 sample/node/sign.js 文件,把 substr 改为 slice
var createNonceStr = function () {
return Math.random().toString(36).substr(2, 15);
return Math.random().toString(36).slice(2, 17);
};
var createTimestamp = function () {
return parseInt(new Date().getTime() / 1000) + "";
};
var raw = function (args) {
var keys = Object.keys(args);
keys = keys.sort();
var newArgs = {};
keys.forEach(function (key) {
newArgs[key.toLowerCase()] = args[key];
});
var string = "";
for (var k in newArgs) {
string += "&" + k + "=" + newArgs[k];
}
string = string.substr(1);
string = string.slice(1);
return string;
};
/**
* @synopsis 签名算法
*
* @param jsapi_ticket 用于签名的 jsapi_ticket
* @param url 用于签名的 url ,注意必须动态获取,不能 hardcode
*
* @returns
*/
var sign = function (jsapi_ticket, url) {
var ret = {
jsapi_ticket: jsapi_ticket,
nonceStr: createNonceStr(),
timestamp: createTimestamp(),
url: url,
};
var string = raw(ret);
jsSHA = require("jssha");
shaObj = new jsSHA(string, "TEXT");
ret.signature = shaObj.getHash("SHA-1", "HEX");
return ret;
};
module.exports = sign;
修改后的文件如下,创建 sign.js 文件,并写入下面的代码
var createNonceStr = function () {
return Math.random().toString(36).slice(2, 17);
};
var createTimestamp = function () {
return parseInt(new Date().getTime() / 1000) + "";
};
var raw = function (args) {
var keys = Object.keys(args);
keys = keys.sort();
var newArgs = {};
keys.forEach(function (key) {
newArgs[key.toLowerCase()] = args[key];
});
var string = "";
for (var k in newArgs) {
string += "&" + k + "=" + newArgs[k];
}
string = string.slice(1);
return string;
};
/**
* @synopsis 签名算法
*
* @param jsapi_ticket 用于签名的 jsapi_ticket
* @param url 用于签名的 url ,注意必须动态获取,不能 hardcode
*
* @returns
*/
var sign = function (jsapi_ticket, url) {
var ret = {
jsapi_ticket: jsapi_ticket,
nonceStr: createNonceStr(),
timestamp: createTimestamp(),
url: url,
};
var string = raw(ret);
jsSHA = require("jssha");
shaObj = new jsSHA(string, "TEXT");
ret.signature = shaObj.getHash("SHA-1", "HEX");
return ret;
};
module.exports = sign;
4. 初始化 git
创建 .gitignore 文件
touch .gitignore
文件 .gitignore 内容如下
node_modules
sample/
git init
git add .
git commit -m 'first commit'
5. 安装 jssha 模块
安装 jssha 模块
npm init -y
npm install jssha@1.5.0 -S
6. 创建 index.js 并引入 helper.js 的 fetchJsapiTicket 和 sign.js
index.js
const { fetchJsapiTicket } = require("./helper.js");
const sign = require("./sign.js");
main();
async function main() {
const jsapi_ticket = await fetchJsapiTicket();
const result = sign(jsapi_ticket, "http://docs.ffffee.com");
// {
// debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
// appId: '', // 必填,公众号的唯一标识
// timestamp: , // 必填,生成签名的时间戳
// nonceStr: '', // 必填,生成签名的随机串
// signature: '',// 必填,签名
// jsApiList: [] // 必填,需要使用的JS接口列表
// }
/*
*something like this
*{
* jsapi_ticket: 'jsapi_ticket',
* nonceStr: '82zklqj7ycoywrk',
* timestamp: '1415171822',
* url: 'http://example.com',
* signature: '1316ed92e0827786cfda3ae355f33760c4f70c1f'
*}
*/
}
7. 添加 nodemon 和 express 代码
https://www.npmjs.com/package/express
npm i express -S
index.js
const express = require("express");
const { fetchJsapiTicket } = require("./helper.js");
const sign = require("./sign.js");
const config = require("./config.json");
const app = express();
app.use(express.json());
app.post("/wechat_share_api/sign", async (req, res) => {
const requestedUrl = req.body.url; // 使用 req.query 而不是 req.params
if (!requestedUrl) {
res.status(400).json({ code: 400, msg: "参数错误" });
return;
}
const jsapi_ticket = await fetchJsapiTicket();
console.log("jsapi_ticket", jsapi_ticket);
console.log("Requested URL", requestedUrl); // 显示获取的 URL
const result = sign(jsapi_ticket, requestedUrl);
/*
*result 返回值如下
*{
* jsapi_ticket: 'jsapi_ticket',
* nonceStr: '82zklqj7ycoywrk',
* timestamp: '1415171822',
* url: 'http://example.com',
* signature: '1316ed92e0827786cfda3ae355f33760c4f70c1f'
*}
*/
res.json({
debug: true,
appId: config.AppID,
url: result.url,
timestamp: result.timestamp,
nonceStr: result.nonceStr,
signature: result.signature,
jsApiList: ["onMenuShareAppMessage"],
});
});
app.listen(3001, () => {
console.log("http://localhost:3001");
});
node index.js
调用尝试签名
http://localhost:3000/sign?url=https://docs.ffffee.com
8. 常见错误以及解决方案
出现下面错误 errcode: 40164,
通过 https://www.ipplus360.com/ 可以查到你自己的 ip,下面根据 https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html 中的文档提示 40164 错误码 是 调用接口的IP地址不在白名单中,请在接口IP白名单中进行设置。
[fetchAccessToken data] {
errcode: 40164,
errmsg: 'invalid ip 123.118.72.95 ipv6 ::ffff:123.118.72.95, not in whitelist rid: 663fa27e-51f60287-3755f633'
}
New token fetched and cached: undefined
[fetchJsapiTicket data] {
errcode: 40001,
errmsg: 'invalid credential, access_token is invalid or not latest, could get access_token by getStableAccessToken, more details at https://mmbizurl.cn/s/JtxxFh33r rid: 663fa27e-6932e051-79781421'
}
在这里有 配置白名单的 第一步:填写服务器配置
:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html
通过 https://www.ipplus360.com/ 可以知道自己的 IP(开发环境)
Error: Chosen SHA variant is not supported
遇到下面的错误,原因是 jssha 版本太高,需要改成 sample 的一样的 "jssha": "^1.5.0"
我的版本 - "jssha": "^3.3.1",
改成 "jssha": "^1.5.0"
Error: Chosen SHA variant is not supported
at new n (C:\Users\Administrator\Desktop\we-share\node_modules\jssha\dist\sha.js:21:21984)
at sign (C:\Users\Administrator\Desktop\we-share\sign.js:42:12)
at C:\Users\Administrator\Desktop\we-share\index.js:16:18
Node.js v18.16.0
npm i jssha@1.5.0 -S
调用尝试签名
http://localhost:3000/sign?url=https://docs.ffffee.com
{
"debug": true,
"timestamp": "1715447994",
"nonceStr": "sl024m87dh",
"signature": "d8eb0570a3f6e0668bcbf2ae1325711b37d94ba7",
"jsApiList": ["updateAppMessageShareData"]
}
出现错误 “fail link must be in js secure domain list”
https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#111
未找到原因,解决办法
使用 onMenuShareAppMessage
替代 updateAppMessageShareData
方法
9. 测试页面获取和使用
微信正常分享给其他用户,步骤如下:
- 把需要分享的页面 url -> 生成二维码
- 用户使用微信扫描二维码 (注意:直接微信打开这个是无法卡片式分享的,目前只能通过扫码后分享才可以)
- 用户点击右上角
...
- 分享给好友 - 选中好友 此时分享成功
测试例子获取:
附录 6-DEMO 页面和示例代码 https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#67
这是 demo 代码 https://www.weixinsxy.com/jssdk/
chrome 浏览器打开
ctrl + S 保存页面
修改例子
修改前
Details
<script>
/*
* 注意:
* 1. 所有的JS接口只能在公众号绑定的域名下调用,公众号开发者需要先登录微信公众平台进入“公众号设置”的“功能设置”里填写“JS接口安全域名”。
* 2. 如果发现在 Android 不能分享自定义内容,请到官网下载最新的包覆盖安装,Android 自定义分享接口需升级至 6.0.2.58 版本及以上。
* 3. 常见问题及完整 JS-SDK 文档地址:http://mp.weixin.qq.com/wiki/7/aaa137b55fb2e0456bf8dd9148dd613f.html
*
* 开发中遇到问题详见文档“附录5-常见错误及解决办法”解决,如仍未能解决可通过以下渠道反馈:
* 邮箱地址:weixin-open@qq.com
* 邮件主题:【微信JS-SDK反馈】具体问题
* 邮件内容说明:用简明的语言描述问题所在,并交代清楚遇到该问题的场景,可附上截屏图片,微信团队会尽快处理你的反馈。
*/
wx.config({
debug: false,
appId: "wxf8b4f85f3a794e77",
timestamp: 1715448905,
nonceStr: "7O21RVGLdDOuZODL",
signature: "b47d87e7531bfca67e128458dc997d2f9cd24ecb",
jsApiList: [
"checkJsApi",
"onMenuShareTimeline",
"onMenuShareAppMessage",
"onMenuShareQQ",
"onMenuShareWeibo",
"onMenuShareQZone",
"hideMenuItems",
"showMenuItems",
"hideAllNonBaseMenuItem",
"showAllNonBaseMenuItem",
"translateVoice",
"startRecord",
"stopRecord",
"onVoiceRecordEnd",
"playVoice",
"onVoicePlayEnd",
"pauseVoice",
"stopVoice",
"uploadVoice",
"downloadVoice",
"chooseImage",
"previewImage",
"uploadImage",
"downloadImage",
"getNetworkType",
"openLocation",
"getLocation",
"hideOptionMenu",
"showOptionMenu",
"closeWindow",
"scanQRCode",
"chooseWXPay",
"openProductSpecificView",
"addCard",
"chooseCard",
"openCard",
],
});
</script>
修改后
Details
<script>
/*
* 注意:
* 1. 所有的JS接口只能在公众号绑定的域名下调用,公众号开发者需要先登录微信公众平台进入“公众号设置”的“功能设置”里填写“JS接口安全域名”。
* 2. 如果发现在 Android 不能分享自定义内容,请到官网下载最新的包覆盖安装,Android 自定义分享接口需升级至 6.0.2.58 版本及以上。
* 3. 常见问题及完整 JS-SDK 文档地址:http://mp.weixin.qq.com/wiki/7/aaa137b55fb2e0456bf8dd9148dd613f.html
*
* 开发中遇到问题详见文档“附录5-常见错误及解决办法”解决,如仍未能解决可通过以下渠道反馈:
* 邮箱地址:weixin-open@qq.com
* 邮件主题:【微信JS-SDK反馈】具体问题
* 邮件内容说明:用简明的语言描述问题所在,并交代清楚遇到该问题的场景,可附上截屏图片,微信团队会尽快处理你的反馈。
*/
const url = window.location.href; // 获取当前的完整URL
const cleanUrl = url.split("#")[0]; // 使用split方法以#为分割点,并取第一部分
const decodedUrl = decodeURIComponent(cleanUrl); // 对URL进行解码
console.log(cleanUrl); // 输出结果
fetch("/wechat_share_api/sign", {
method: "POST", // *GET, POST, PUT, DELETE, etc.
body: JSON.stringify({ url: decodedUrl }),
})
.then((res) => {
console.log("res", res);
return res.json();
})
.then((result) => {
console.log("[result]", result);
wx.config({
debug: true,
appId: result.appId,
timestamp: Number(result.timestamp),
nonceStr: result.nonceStr,
signature: result.signature,
jsApiList: [
"checkJsApi",
"onMenuShareTimeline",
"onMenuShareAppMessage",
"onMenuShareQQ",
"onMenuShareWeibo",
"onMenuShareQZone",
"hideMenuItems",
"showMenuItems",
"hideAllNonBaseMenuItem",
"showAllNonBaseMenuItem",
"translateVoice",
"startRecord",
"stopRecord",
"onVoiceRecordEnd",
"playVoice",
"onVoicePlayEnd",
"pauseVoice",
"stopVoice",
"uploadVoice",
"downloadVoice",
"chooseImage",
"previewImage",
"uploadImage",
"downloadImage",
"getNetworkType",
"openLocation",
"getLocation",
"hideOptionMenu",
"showOptionMenu",
"closeWindow",
"scanQRCode",
"chooseWXPay",
"openProductSpecificView",
"addCard",
"chooseCard",
"openCard",
],
});
})
.catch((error) => {
console.error("[error]", error);
});
</script>
解决微信分享 config:fail,invalid signature
- 有中文参数 - 签名失败的问题
错误提示: config:fail,invalid signature
正确的方式:
- 前端显示的路径,如果存在中文参数,那么需要 使用
encodeURIComponent
对中文字符串进行编码
假如有这样子的 url
const str = `http://localhost:5173/detail.html?path=/api/image/p2904477111.webp&title=飞驰人生2®ion=中国大陆&genres=剧情喜剧运动&director=韩寒`;
需要改成
const title = encodeURIComponent("飞驰人生2");
const region = encodeURIComponent("中国大陆");
const genres = encodeURIComponent("剧情喜剧运动");
const director = encodeURIComponent("韩寒");
const str = `http://localhost:5173/detail.html?path=/api/image/p2904477111.webp&title=${title}®ion=${region}&genres=${genres}&director=${director}`;
- 前端发送给后端的参数(一般 post 请求) 内容如下, 后端无需
decodeURIComponent
{
"url": "https://docs.ffffee.com/wechat/mobile/detail.html?path=/api/image/p2903145026.webp&title=%E7%AC%AC%E4%BA%8C%E5%8D%81%E6%9D%A1®ion=%E4%B8%AD%E5%9B%BD%E5%A4%A7%E9%99%86&genres=%E5%89%A7%E6%83%85%E5%96%9C%E5%89%A7%E5%AE%B6%E5%BA%AD&director=%E5%BC%A0%E8%89%BA%E8%B0%8B"
}