微信 H5 怎样接入 JS-SDK?签名和网页授权要注意什么?
页面调用 wx.config 时要传一个签名,签名是把 jsapi_ticket、noncestr、timestamp 和当前页面 url 按字段名排序拼成字符串后做 sha1。签名和 ticket、网页授权的 code 换 openid 都必须在服务端完成,因为它们依赖不能暴露的 AppSecret。snsapi_base 静默拿 openid,snsapi_userinfo 要用户同意才能拿昵称头像。
简答
在微信里打开的 H5 页面,想调用分享、扫一扫、选图片这些能力,要先引入 JS-SDK,再调用
wx.config告诉微信“我是哪个公众号,要用哪些接口”。微信会校验一个签名,通过了才放行,接口要放在wx.ready里调用。签名的算法是:把
jsapi_ticket、noncestr、timestamp和当前页面的url这四个字段按字段名的 ASCII 顺序排好,拼成key=value&key=value的字符串,再做 sha1。url要去掉#及后面的部分,noncestr和timestamp必须和传给wx.config的一致。签名必须在服务端算。因为
jsapi_ticket要用access_token换,access_token又要用公众号的 AppSecret 换,这个密钥一旦放到前端,任何人都能冒充你的公众号调接口。两者有效期都是 7200 秒,官方要求在服务端全局缓存。想知道当前用户是谁,要用网页授权。
snsapi_base是静默授权,只能拿到 openid;snsapi_userinfo要用户手动同意,才能拿到昵称和头像。微信回调时给页面一个code,用code换 openid 同样要带 AppSecret,所以也在服务端做。code只能用一次,5 分钟不用就过期。
前端怎样调用 wx.config
页面先引入 JS-SDK 的脚本,然后向自己的服务端要签名。请求时把当前页面地址传过去,服务端算好返回:
async function setupWechat() {
// 签名用的 url 不含 # 及后面的部分。
const url = location.href.split('#')[0];
const res = await fetch(`/api/wechat/signature?url=${encodeURIComponent(url)}`);
const { appId, timestamp, nonceStr, signature } = await res.json();
wx.config({
debug: false,
appId,
timestamp,
nonceStr,
signature,
jsApiList: ['updateAppMessageShareData', 'scanQRCode'],
});
wx.ready(() => {
// 页面加载时就要调用的接口放在这里,确保签名校验已经完成。
});
wx.error((err) => {
console.error('JS-SDK 签名校验失败', err);
});
}
为什么页面一加载就要调的接口必须放进 wx.ready?因为 wx.config 是异步校验的,调用它以后结果还没回来。官方文档说,用户点击后才触发的接口可以直接调,那时校验早就完成了。
wx.error 在校验失败时触发,比如签名过期。官方建议单页应用可以在这里重新获取签名。单页应用切换路由后 URL 变了,同一个 URL 只需要配置一次,URL 变化后要用新地址重新签名、重新调用 wx.config。
另外,页面的域名要先在公众号后台的“JS 接口安全域名”里配置,不支持 IP 地址、端口号和短链域名,最多 5 个。域名不对,签名算对了也调不通。
签名字符串怎么拼
下面用官方文档里的示例值演示拼接顺序。实际签名在服务端用 sha1 计算,这里只演示 sha1 之前的那个字符串:
function buildSignString(fields) {
return Object.keys(fields)
.sort()
.map((key) => `${key}=${fields[key]}`)
.join('&');
}
const fields = {
noncestr: 'Wm3WZYTPz0wzccnW',
jsapi_ticket: 'sM4AOVdWfPE4DxkXGEs8VMCPGGVi4C3VM0P37wVUCFvkVAy_90u5h9nbSlYy3-Sl-HhTdfl2fzFy1AOcHKP7qg',
timestamp: 1414587457,
url: 'http://mp.weixin.qq.com?params=value',
};
const signString = buildSignString(fields);
console.log(Object.keys(fields).sort().join(',')); // jsapi_ticket,noncestr,timestamp,url
console.log(signString.endsWith('×tamp=1414587457&url=http://mp.weixin.qq.com?params=value')); // true
console.log(signString.includes('%')); // false
第一行输出是排序后的字段顺序。对象里写的是 noncestr 在前,排完序 jsapi_ticket 跑到了最前面,因为 j 的 ASCII 码比 n 小。官方要求按字段名的字典序排,顺序错了签名就对不上。
为什么要检查有没有 %?官方规定字段名和值都用原始值,不做 URL 转义。如果拼接时用了 URLSearchParams 之类的工具,url 里的 :、/、? 会被编码,签出来的结果就不对。字段名也必须全小写,所以这里写的是 noncestr,而传给 wx.config 的是驼峰的 nonceStr,两处的值要相同。
这个字符串做 sha1 后,得到的是官方文档给出的 0f9de62fce790f9a083d5c99e95740ceb90c27ed,用 Node.js 22 的 crypto.createHash('sha1') 算出的结果一致。
为什么签名必须放在服务端
签名用到的 jsapi_ticket 是这样来的:服务端用 AppID 和 AppSecret 换 access_token,再用 access_token 换 jsapi_ticket。AppSecret 相当于公众号的密码,放到前端就等于公开了,别人可以拿它调用你公众号的所有接口。所以官方明确要求签名逻辑在服务器端实现。
还有一个原因是频率限制。access_token 和 jsapi_ticket 有效期都是 7200 秒,官方说获取 jsapi_ticket 的接口调用次数非常有限,要求两者都在服务端全局缓存,否则上线后会触发频率限制。如果每个用户打开页面都去换一次,很快就会超限。
网页授权怎样拿到 openid
页面需要知道当前用户是谁时,把用户重定向到微信的授权地址:
https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
scope 填 snsapi_base 时是静默授权,用户无感知,直接跳回 redirect_uri,只能拿到 openid。填 snsapi_userinfo 时要用户手动同意,同意后能拿到昵称、头像。参数顺序不能乱,官方说参数顺序不对,授权页面无法正常访问;redirect_uri 要做 URL 编码。
微信跳回来时,地址上带着 code 和原样返回的 state。前端把 code 交给服务端,服务端带上 AppSecret 调 sns/oauth2/access_token 换出 openid 和网页授权的 access_token。官方要求 AppSecret 和换来的 token 只保存在服务器,不能传给客户端。注意这个网页授权的 access_token 和前面 JS-SDK 用的基础 access_token 不是同一个东西,它只能用来获取这个用户的信息。
code 只能用一次,5 分钟没用就自动过期。所以前端拿到 code 后要马上交给服务端,页面刷新时也不要拿旧的 code 再请求一次。
常见追问
签名校验总是报 invalid signature,先查什么?
先看参与签名的
url和页面实际地址是否完全一致,包括有没有去掉#后面的部分、查询参数是否一样。再看noncestr、timestamp是否和传给wx.config的相同,以及服务端拼接时有没有对值做 URL 转义。最后确认jsapi_ticket没有过期,域名已经配置在 JS 接口安全域名里。
为什么不建议一打开页面就跳 snsapi_userinfo 授权?
微信从 2022 年起调整了规则:没有用户操作就直接发起
snsapi_userinfo授权,会进入网页快照页模式,页面被固化,用户要点“点击访问完整网页”才能继续。官方不推荐打开网页就直接授权。所以先用snsapi_base拿 openid,等用户点了需要个人信息的按钮再发起snsapi_userinfo。
state 参数有什么用?
微信重定向回
redirect_uri时会原样带上state,官方说明它用于防止越权行为,取值只能是字母和数字,最多 128 字节。常见做法是发起授权前生成一个随机值存在会话里,回调时比对,不一致就拒绝,这样别人伪造的回调就用不上。