H5 拉起微信小程序流程解析

非个人主体并且已认证的(微信认证)小程序,使用云开发静态网站托管的网页,可以免鉴权跳转任意合法合规的小程序。即可以在微信内部浏览器的 H5 跳转小程序,也可以在微信外部浏览器或其他部分 App (如企业微信、QQ 等)跳转微信小程序(参见微信文档:静态网站 H5 跳小程序)。

微信浏览器内

由于 H5 拉起小程序是在微信浏览器封闭的系统内,微信有着较高的定制和管控能力。

在实现方案上,微信未提供直接拉起小程序的 API,而是提供了:供用户点击跳转小程序的开放标签 (从 2020 年中开始提供)。使用方法如下:

<wx-open-launch-weapp
  id="launch-btn"
  username="gh_xxxxxxxx"
  path="pages/home/index?user=123&action=abc"
>
  <template>
    <style>.btn { padding: 12px }</style>
    <button class="btn">打开小程序</button>
  </template>
</wx-open-launch-weapp>

在需要用到跳转能力的页面中,首先引入微信 js-sdk,并在期望用户点击跳转的地方引入该开放标签即可。其中,gh_xxxxxxxx 为目标跳转的小程序原始 ID,线上小程序均可在”更多资料“一栏查看。

另外,在使用该标签的页面中,还需首先调用 wx.config 进行鉴权,否则该标签不展示。下面详细分析其流程。

获取 signature

首先通过服务端获取到了主体使用权的签名 signature。

(注意这里获取 signature 中使用的 appId 与开放标签中使用的 appId 不是同一个:这里用的是主体服务号的 appId,开放标签中使用的是跳转的目标小程序的 appId)。

签名校验

接着将 signature 作为参数供 wx.config 调用。 方法传入参数为:

wx.config({
  // debug: true, // 调试时可开启
  appId: "必填,公众号的唯一标识", // <!-- replace -->
  timestamp: 0, // 必填,填任意数字即可
  nonceStr: "nonceStr", // 必填,填任意非空字符串即可
  signature: signature, // 必填,填服务端获取的签名
  jsApiList: ["chooseImage"], // 必填,随意一个接口即可
  openTagList: ["wx-open-launch-weapp"], // 填入打开小程序的开放标签名
});

调用该方法后,经过多处调用,最终进入下面的逻辑:

o.WeixinJSBridge
  ? WeixinJSBridge.invoke(n, x(e), function (e) {
      A(n, e, i);
    })
  : B(n, i);

其中 X(e) 为拼接 signature 字段等。看 invoke 方法,其中 n 为 'preVerifyJSAPI' 方法。

invoke 方法在 js-sdk 中找不到定义,想到微信 app 可以加载执行 JS 代码,于是解压了微信 apk,找到 assets/wxjs.js,在里面找到了定义:

WeixinJSBridge.invoke = function _call(func, params, callback) {
  // 省略
  var msgObj = {
    func: func,
    params: params,
  };
  msgObj[_MESSAGE_TYPE] = "call";
  msgObj[_CALLBACK_ID] = callbackID;

  try {
    _sendMessage(JSON.stringify(msgObj));
  } catch (e) {
    __initLog(__EL, "_call error", e);
  }
};

核心为 _sendMessage 方法:

function _sendMessage(msg) {
  // 省略 msg -> msgArrayString
  __wx._sendMessage(msgArrayString);
}

可以看到最终是将传入的参数调用 __wx._sendMessage 方法,其中 __wx 对象从 window.__wx 上取得,大概率是 Native 上定义的对象,并注入到 JS 环境中。

(注:JS 调用 Native 方法可参见:H5 与 Native 的交互方案,看 wxjs.js 中,暂时用的 addJavascriptInterface 方案,但 iframe 发起 URL Schema 的方式也写了做备份。)

鉴权结果回调

鉴权结果要通知 JS 侧,需要 Native 调用 H5 的方法,来看其中的 _handleMessageFromWeixin 方法:

function _handleMessageFromWeixin(message) {
  // 省略
  switch (msgWrap[_MESSAGE_TYPE]) {
    case "callback":
      {
        if (
          typeof msgWrap[_CALLBACK_ID] === "string" &&
          typeof _callback_map[msgWrap[_CALLBACK_ID]] === "function"
        ) {
          var ret = _callback_map[msgWrap[_CALLBACK_ID]](msgWrap["__params"]);
          delete _callback_map[msgWrap[_CALLBACK_ID]]; // can only call once
          return JSON.stringify(ret);
        }
        return JSON.stringify({
          __err_code: "cb404",
        });
      }
      break;
    case "event":
    // 省略
  }
}

可以看到在这里执行 callback() 回调,并传入 Native 处理结果,wx.config 的回调方法为 A(n, e, i),没有太多重要的操作。

在实际页面中发现,如果是鉴权不通过,那么开放标签不会显示,所以也研究了下微信的开放标签

开放标签像是 webComponent,但很可惜没有在解压的微信 apk 中找到对应的定义,应该是从浏览器底层支持了。

小结

在微信内部浏览器中,通过鉴权的网站使用 <wx-open-launch-weapp> 标签即可拉起小程序。而通过上面流程的梳理发现,只要服务号主体配置了安全域名,那么理论上这个页面,可以在微信内置浏览器内跳转任意小程序。网上有相关尝试证实确实如此。暂时没发现有做其它管控。

其它浏览器

如前所述,<wx-open-launch-weapp> 是微信内提供的标签,在其它浏览器中无法使用,但官方也提供了其它浏览器拉起小程序的方法,即使用 URL Scheme 的方式。

外部网页使用: location.href = "weixin://dl/business/?t= *TICKET*" 的方式即可拉起小程序。

其中 TICKET 的获取方式与上文中 signature 类似,是在三方服务端通过 token 去请求小程序服务端获得,需要的参数是 appId 和 AppSecret ,在这里使用的 appId 和 AppSecret 即为将要拉起的目标小程序。可以选择到期失效,也可以选择永久有效。

所以三方媒体页面要拉起某个小程序,需要首先获得该小程序的许可,拿到其 TICKET 才行。但 TICKET 可以选择永久有效,所以从原理上说,如果是永久有效,存在被另外主体获取并盗用的可能。

获取 TICKET 的过程并没有用到安全域名配置的信息,所以任意的网站只要能拿到 TICKET 均可以调起小程序。(另外,URL Scheme 的方式也可用于从其它 app 中调起打开小程序。)

另外,由于是使用 location.href = "weixin://dl/business/?t= *TICKET*" 的方式调起小程序,所以理论上是网页可以自动拉起,实测也确实如此。

小结

外部浏览器通过 URL Scheme 的方式调起小程序,无论什么网站,只要能生成对应小程序的 TICKET 即可拉起该小程序,几乎没有管控。

总结

2020 年中,微信开放的拉起小程序的能力,在微信浏览器内做的管控为:需 为媒体网站配置安全域名 + 用户点击 方可跳转小程序;外部浏览器通过 URL Scheme 的方式做的管控为:需事先获取目标小程序的 TICKET,但整体来说该方式调起小程序则相对灵活,且可以由网页自动拉起小程序(需要说明的是,个人小程序,不具有获取 TICKET 或 signature 的权限)。

原文首发于:H5 拉起微信小程序流程解析 | 宜想悠然亭

您可能感兴趣的文章