微信 jssdk 踩坑实录(nodejs实现后台)

1

最近做 H5 有一个需求,要在网页链接分享到微信好友的显示描述和自定义缩略图,并在分享到朋友圈的时候显示缩略图.开始我觉得这是一个很简单的需求,应该在网页上配置一下就好了,后来才发现自己的真的是 too young,我花了一天半的时间来处理这个问题,最终得到了完美的解决,这里我记录下自己的踩坑经历,以便日后查阅.

2

微信官方是这么说的:

为规范自定义分享链接功能在网页上的使用,自 2017 年 4 月 25 日起,JSSDK“分享到朋友圈”及“发送给朋友”接口,自定义的分享链接,其域名或路径必须与当前页面对应的公众号 JS 安全域名一致,否则将调用失败。例如,当前页面是 http://www.abc.com/123,其公众号对应的JS安全域名为 www.abc.com 以及 www.xyz.com,则分享自定义链接 http://www.abc.com/456 可以成功,分享 http://www.xyz.com/123http://www.def.com/123 均将失败。对于未接入微信 JSSDK 或已接入但 JSSDK 调用失败的网页,被用户分享时,分享卡片将统一使用默认缩略图和标题简介,不允许自定义。

意思是如果要自定义缩略图和简介,我们就需要接入微信的 jssdk.好,那我们现在就开始接入辣鸡微信的 jssdk.

微信文档地址:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#2

3.准备工作

如果要接入微信的 jssdk,必要条件如下,如果缺少其中任意一个,就不用继续看下去了,因为不能接入 jssdk:

  • 一个认证了的微信公众号(需要公众号的 appid 和 screct)
  • 一个已经备案的域名

可选的配置:

  • 后台开发语言(这里我选用了 nodejs).
  • js 安全域名(要求 80/443 端口),这个后面还会提到.
  • ssl 证书(用于添加 https,这里推荐用 https,防劫持这样的有点就不说了,还有一个很重要的原因就是:如果不是 https 的话,使用微信浏览页面会有一个 confirm 安全的页面,确认以后虽然可以进入 h5,但是在 ios 下底部会有一个难看的导航栏,十分影响体验)

4.微信公众号配置

(1)ip 白名单配置

ip 白名单主要是用于后端根据 appid 和 screct 获取 access_token,只有在白名单中的 ip 才可以访问微信的官方接口.
WX20200121-093121@2x.png

(2)添加 js 安全域名


需要注意的是:

  • 安全域名最多只能填写三个,而且一个月内最多可以修改并保存三次.
  • 和上面提到的一样,这个域名必须是备案过的;
  • 需要下载微信提供的 txt 验证文件上传到 web 服务器网页的路径下,例如:我使用 vue(webpack)打包的文件在 https://example.com/nh 下,那么这个文件也存放在这个路径下,确保微信可以虎获取到这个文件就可以,因为如果获取不到是无法添加域名的.(这一步其实是为了限制一个认证公众号可以绑定的域名数量)

5 后端服务开发(使用 nodejs)

使用 koa 框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
var http = require("http");
var https = require("https");
var fs = require("fs");

const Koa = require("koa");

//使用 axios 向微信服务器发起请求
const axios = require("axios");

const app = new Koa();
const cors = require("koa-cors");
const utils = require("./utils/utils");

//config 中包含微信公众号的 appid 和 screct
//这两条数据很重要,不能泄露
const config = require("./config/config");

//引入微信 jssdk校验代码
//微信开发者文档中中包含了python/java/php/nodejs 的代码
//这里我选择了 nodejs 的生成签名的包
var sign = require("./sign.js");

//这里是从阿里云申请的免费 ssl 证书,方便为接口添加 https
var options = {
key: fs.readFileSync("./https/3400951_wxshare.cf.key"), //ssl文件路径
cert: fs.readFileSync("./https/3400951_wxshare.cf.pem") //ssl文件路径
};
//添加跨域相关信息
app.use(
cors({
origin: function() {
return "*";
},
exposeHeaders: ["WWW-Authenticate", "Server-Authorization"],
maxAge: 5,
credentials: true,
allowMethods: ["GET", "POST"],
allowHeaders: ["Content-Type", "Authorization", "Accept"]
})
);
app.use(async ctx => {
if (ctx.request.query.url2) {
//根据公众号的 appid和 screct 获取 access_token
let [err, data] = await utils.awaitWrap(
axios.get(
`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${config.config.appId}&secret=${config.config.secret}`
)
);
if (err) {
return utils.res(ctx, 1001, "查询 access-token 失败");
} else {
if (data.data.access_token) {
//根据 access_token 换取 ticket
let [err2, data2] = await utils.awaitWrap(
axios.get(
`https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${data.data.access_token}&type=jsapi`
)
);
if (err2) {
return utils.res(ctx, 1002, "查询 ticket 失败");
} else {
//根据 ticket 以及前端传过来的 url,生成signature等若干信息返回给前端进行 jssdk 验证.
let signature = sign(
data2.data.ticket,
decodeURIComponent(ctx.request.query.url2)
);
signature.appId = config.config.appId;
console.log(signature);
//返回给前端
return utils.res(ctx, 200, "success", signature);
}
} else {
return utils.res(ctx, 1001, data.data.errmsg);
}
}
} else {
return utils.res(ctx, 1003, "没有参数");
}
});
//创建http服务并监听 80 端口
//创建 https 服务并监听 443 端口
http.createServer(app.callback()).listen(3000);
https.createServer(options, app.callback()).listen(3001);

需要注意的事项:

  • 1.微信获取 access_token 和 ticket 的接口是有访问频率限制的,据称是每天 2000 次.

生成签名之前必须先了解一下 jsapi_ticket,jsapi_ticket 是公众号用于调用微信 JS 接口的临时票据。正常情况下,jsapi_ticket 的有效期为 7200 秒,通过 access_token 来获取。由于获取 jsapi_ticket 的 api 调用次数非常有限,频繁刷新 jsapi_ticket 会导致 api 调用受限,影响自身业务,开发者必须在自己的服务全局缓存 jsapi_ticket 。参考以下文档获取 access_token(有效期 7200 秒,开发者必须在自己的服务全局缓存 access_token):https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html用第一步拿到的access_token 采用 http GET 方式请求获得 jsapi_ticket(有效期 7200 秒,开发者必须在自己的服务全局缓存 jsapi_ticket):https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi

  • 2.签名算法

    签名生成规则如下:参与签名的字段包括 noncestr(随机字符串), 有效的 jsapi_ticket, timestamp(时间戳), url(当前网页的 URL,不包含#及其后面部分) 。对所有待签名参数按照字段名的 ASCII 码从小到大排序(字典序)后,使用 URL 键值对的格式(即 key1=value1&key2=value2…)拼接成字符串 string1。这里需要注意的是所有参数名均为小写字符。对 string1 作 sha1 加密,字段名和字段值都采用原始值,不进行 URL 转义。即 signature=sha1(string1)。

这里我直接用了微信给的 example 中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//sign.js
var createNonceStr = function() {
return Math.random()
.toString(36)
.substr(2, 15);
};

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

可以在这里验证生成微信签名的正确性:http://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=jsapisign

6.前端开发

(1) 引入 JS 文件

在需要调用 JS 接口的页面引入如下 JS 文件,(支持 https):http://res.wx.qq.com/open/js/jweixin-1.6.0.js

如需进一步提升服务稳定性,当上述资源不可访问时,可改访问:http://res2.wx.qq.com/open/js/jweixin-1.6.0.js (支持 https)。

(2)通过 config 接口注入权限验证配置

所有需要使用 JS-SDK 的页面必须先注入配置信息,否则将无法调用(同一个 url 仅需调用一次,对于变化 url 的 SPA 的 web app 可在每次 url 变化时进行调用,目前 Android 微信客户端不支持 pushState 的 H5 新特性,所以使用 pushState 来实现 web app 的页面会导致签名失败,此问题会在 Android6.2 中修复)。

1
2
3
4
5
6
7
8
wx.config({
debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: '', // 必填,公众号的唯一标识
timestamp: , // 必填,生成签名的时间戳
nonceStr: '', // 必填,生成签名的随机串
signature: '',// 必填,签名
jsApiList: [] // 必填,需要使用的JS接口列表
});

(3)通过 ready 接口处理成功验证

1
2
3
wx.ready(function() {
// config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。
});

(4)通过 error 接口处理失败验证

wx.error(function(res){
// config 信息验证失败会执行 error 函数,如签名过期导致验证失败,具体错误信息可以打开 config 的 debug 模式查看,也可以在返回的 res 参数中查看,对于 SPA 可以在这里更新签名。
});

样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
//index.html
//使用 fetch 获取接口数据
//同时注意 webpack 没有编译 index.html,所以 es6 语法并不支持
<script src=https://res2.wx.qq.com/open/js/jweixin-1.6.0.js></script>
<script>
//这里请求之前写好的后端接口,发送当前页面的 url
//url 必须使用window.location.href.split("#")[0] 获取
var wxshareAuthAddress = "https://example.com:3001?url2=" + encodeURIComponent(window.location.href.split("#")[0]);
fetch(wxshareAuthAddress)
.then(res => {
res.json().then(res => {
//根据获取到的结果验证权限
wx.config({
debug: false,
appId: res.data.appId,
timestamp: res.data.timestamp,
nonceStr: res.data.nonceStr,
signature: res.data.signature,
jsApiList: ["updateTimelineShareData", "updateAppMessageShareData"] // 必填,需要使用的JS接口列表
});
//验证成功以后出发的方法
wx.ready(function(res) {
console.log("ready");
//updateAppMessageShareData是分享到微信消息的api
wx.updateAppMessageShareData({
title: "", // 分享标题
desc: "", // 分享描述
link: "/", // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
imgUrl: "", // 分享图标
success: function() {
// 设置成功
console.log("设置发送给微信好友消息成功");
}
});
// console.log(res);
// config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。

//分享到朋友圈设置的 api
wx.updateTimelineShareData({
title: "", // 分享标题
link: "", // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
imgUrl: "", // 分享图标
success: function(res) {
// 设置成功
console.log("设置朋友圈分享成功");
}
});
// wx.showMenuItems({
// menuList: ["menuItem:readMode", "menuItem:share:facebook"] // 要显示的菜单项,所有menu项见附录3
// });
});
wx.error(function(res) {
console.log("error" + res);
// config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。
});
});
})
.catch(err => {
console.log(err);
});</script>

7 问题汇总

运维

https

注意接口一定要添加 https,而且绑定域名.
如果不绑定域名,使用 https://ip这样的形式,使用微信开发者工具调试时接口不会报错,但是在手机端会拦截请求.
证书可以到阿里云上申请,域名可以到 freedom 申请免费域名,然后在 dnspod 添加域名解析以及证书与域名的绑定关系;

WX20200121-120951@2x.png

部署服务

我使用了 docker 部署了服务,这样可以免去配置环境等问题.

1
2
3
4
5
6
7
8
9
10
11
# Dockerfile
FROM node
RUN mkdir -p /home/Service
WORKDIR /home/Service
COPY . /home/Service
RUN npm install
EXPOSE 3000
EXPOSE 3001
CMD node ./serve.js
## 如果想运行多条指令可以这样:
## CMD git pull && npm install && npm start

暴露端口

部署在阿里云上的主机记得开放端口方便外部访问:
WX20200121-120715@2x.png

前端开发

invalid signature

1.url

绝大多数invalid signature问题的原因都是 url 的问题,
url 必须与之前填入的 js 安全域名相同.

确保 url 是动态生成的,同时一定要使用 encodeUrlComponent编码!!!

1
encodeURIComponent(window.location.href.split("#")[0]);

2.确保签名算法无误

签名生成规则如下:

参与签名的字段包括有效的 jsapi_ticket(获取方式详见微信 JSSDK 文档), noncestr (随机字符串,由开发者随机生成),timestamp (由开发者生成的当前时间戳), url(当前网页的URL,不包含#及其后面部分。注意:对于没有只有域名没有 path 的 URL ,浏览器会自动加上 / 作为 path,如打开 http://qq.com 则获取到的 URL 为 http://qq.com/)。
对所有待签名参数按照字段名的 ASCII 码从小到大排序(字典序)后,使用 URL 键值对的格式(即key1=value1&key2=value2…)拼接成字符串 string1。这里需要注意的是所有参数名均为小写字符。
接下来对 string1 作 sha1 加密,字段名和字段值都采用原始值,不进行 URL 转义。即 signature=sha1(string1)。

后端开发

api 调用限制

微信对 access_token 以及 ticket 的获取有每日调用次数的限制,对于稍大的项目就需要考虑缓存 ticket(ticket 的有效期是 7200s),这一点微信文档中有详细的说明.

安全相关

appid 和 screct 不能轻易泄露,返回给前端的数据中需要传 appid.

thank u !