1. 背景
老生常谈,小程序可以通过微信官方提供的登录能力方便地获取微信提供的用户身份标识,快速建立小程序内的用户体系,即 「静默登录」 。作者在之前三篇文章《图解小程序静默登录那些事》、《小程序静默登录方案设计》、《小程序用户登录架构设计》中提到过,通过对前端监控上报数据的分析,发现静默登录的成功率和耗时存在很大提升的空间。统计了下静默登录请求失败的原因,发现 71.4% 的失败案例都是因为请求超时问题,“罪魁祸首”还是小程序端用临时登录凭证code 换取 auth-token 和 用户信息 这个接口,该接口的调用链路长导致耗时高。开发者通过调用 wx.login 获取到 code ,将其发送到开发者后端,开发者后端调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 和 会话密钥 session_key,进行关联自定义登录态(auth-token)、初始化用户信息等操作后才将请求结果返回给小程序。
理想情况下,静默登录成功 已经为每一个用户创建独一无二的业务身份。然而,对于复杂的跨端应用,比如一个包含pc、h5、小程序三端的电商应用,不同渠道注册的用户身份(uid)是不同的,用户登录后难以对各个渠道的交易、促销、收藏等数据进行整合。因此,要实现跨端的用户体系数据互通,就需要提供一个唯一的用户标识——手机号,即 「用户登录」。最常见的用户登录方案是让用户点击 button 按钮来触发授权手机号弹窗,待用户同意授权后,小程序端将加密的手机号数据发送到开发者后端,开发者后端使用session_key(对称解密密钥)进行对称解密,获取手机号来绑定用户id,并更新用户身份(游客->普通用户),最后将更新后的用户信息数据返回给小程序端。然而,session_key是存在有效期的,因此,在小程序端发起登录之前需要调用微信开放接口wx.checkSession校验 session_key 是否有效,无形中增加了登录请求链路的复杂度,提高了用户登录的耗时。
从上述「静默登录」和「用户登录」两个案例中可知,出于安全性考虑,调用微信开放能力需要额外的请求获取签名、密钥进行鉴权,无形中使调用链路更加复杂,提高了请求耗时,降低了用户体验。
那么如何优化这两条链路呢?小程序方提出了云开发解决方案。
云开发 是集成于小程序控制台的原生 Serverless 云服务。其核心功能包括:云函数、云数据库和云存储。云开发的云函数的独特优势在于与微信登录鉴权的无缝整合。从小程序端调用云函数时,开发者可以在云函数内使用 wx-server-sdk 提供的 getWXContext 方法获取到每次调用的上下文(appid、openid、unionid 等),无需维护复杂的鉴权机制,即可获取天然可信任的用户登录态。同时腾讯云提供私有网络(Virtual Private Cloud,VPC)为 云服务器、云数据库 等资源构建逻辑隔离的、用户自定义配置的网络空间,从而保证安全和隔离性,并且能够根据请求自动伸缩。不同的VPC之间可以通过对等链接、云联网实现高速的云上资源互通,从而缩短链路请求时间。据我们统计,改造成云开发的静默登录请求时间缩短 45%以上,一次登录请求成功率也从原来的 97.78%提升到 99.98%。
或许有人会提出质疑,小程序云开发流程已经日益成熟且案例丰富,是否有必要花篇幅去写一篇登录改造实战经验。其实我们所在的业务场景更为复杂,是基于微信小程序的电商 saas 产品,致力于提供全面、可靠的小程序商城经营服务。作为服务商,批量支持 500+商家的云开发接入是一个浩大的工程,也是小程序第三方批量代云开发的首批实践者,团队摸着石头过河,期间踩坑无数,希望能够总结一些经验,反哺微信生态发展。
因此,本章篇幅主要侧重以下两个方面:一是阐述批量代云开发模式的整体架构和运行流程。二是通过典型的登录案例改造云开发前后数据对比,验证云开发优势。
2. 了解第三方批量代云开发
2.1 普通小程序云开发
普通小程序开发模式只需在微信公众平台(以下简称为mp)填写相关信息注册小程序,获取小程序的AppId,在开发者工具填写对应的AppId,对小程序功能进行开发与调试,并将代码上传至对应小程序的 mp 版本管理后台,然后人工进行提交审核和发布。
如果小程序想要接入云开发,可在开发者工具点击选择“微信云开发”并勾选同意"云开发服务条款",点击“云开发按钮”初始化云环境,一个环境对应一整套独立的云开发资源,包括数据库、存储空间、云函数等资源。在实际开发中,建议每一个正式环境都搭配一个测试环境,所有功能先在测试环境测试完毕后再上到正式环境。可在wx.cloud.init入参env中传入线上云环境 id,确保线上调用的是正式环境。开发者无需管理服务器,可在开发工具内自由创建云函数,进行编写、一键上传部署即可运行云函数代码。
自此,发布在线上的小程序正常调用正式环境的云函数,可见,云函数的部署与小程序的发布流程是完全解耦的。
2.2 第三方小程序开发
第三方小程序开发流程如上图所示,小程序运营者可以一键授权给第三方平台,通过第三方平台来完成业务。也可以完全托管给第三方,由第三方平台代为注册小程序。商家授权的小程序定义为“商家小程序”,另一种“开发小程序”指的是服务商用于开发用途的小程序账号,该账号通常不会发布上线,仅仅用于登录微信开发者工具进行代码编写&提交。服务商使用“开发小程序”进行开发与调试,通过开发者工具上传的代码会提交到第三方平台的草稿箱,权限较高的管理者可以通过第三方管理后台调用接口将草稿箱的代码提交到模板库,模板库的代码就可以批量提交给千千万万的商家小程序使用,同样也可以批量调用接口帮商家提审和发布。
2.3 批量代云开发
批量代云开发模式,是针对服务商批量开发部署小程序模版场景提供的开发模式。在该模式下,服务商可为名下已获得商户授权的小程序创建云环境,云环境资源归属于第三方平台,由服务商进行资源的购买维护。并且在该模式下提供批量操作接口,进行批量开发。
批量代云开发模式下仍然是使用“开发小程序”开发和调试小程序代码和云函数,测试完成后,可将云函数打包上传到第三方云函数管理库。
这里,我们衍生 “代码库” 的概念,即对模板库的代码进行业务层面的封装,以往是直接将模板库代码上传给商家小程序,这并不能满足商家差异化的需求,举个例子,假设只有部分商家愿意开通云开发功能,或者商家只愿意调用个别云函数功能,那么为所有商家创建云环境且部署第三方云函数管理库的所有云函数显然会造成严重的资源浪费。“代码库” 就可以解决这个问题,在模板库代码转入到代码库之前,会选择该代码所需关联的云函数列表,最后成功转入第三方代码库。
众所周知,由第三方在批量代云开发模式下创建的云环境的所有权隶属于第三方,第三方AppID是承载批量云开发模式的账号 ID,关联对应唯一的腾讯云账号用来承载该模式下的所有云资源,它是通过绑定环境接口将该 环境共享 给客户小程序使用,因此,必须在小程序代码云环境初始化函数wx.cloud.Cloud中显式指明资源方环境 ID 和第三方 AppID,否则小程序后端资源调用的会是原有客户小程序的云环境资源,导致服务异常。
服务商为每一个商家小程序创建云环境,它们资源方环境 ID都是独一无二的,对于这类差异化的配置,可以在为每个商家上传代码时,将其写入ext.json文件。所以,在代码库的代码上传到商家小程序时,执行了以下逻辑:
- 判断该商家是否同意开通云开发,如若同意,判断该商家小程序是否绑定云环境,如若未绑定云环境,则帮商家创建并绑定云环境。
- 遍历当前代码库所关联的云函数列表,为商家所在云环境创建或者更新云函数代码,如若是创建云函数,还需要更新云函数配置,比如私有网络配置、公网访问配置等等。
- 将资源方环境 ID、第三方 AppID以及其他业务所需差异配置信息写入
ext.json,并注入小程序代码包,上传到商家小程序,提审发布后小程序端可以通过调用wx.getExtConfigSync()或wx.getExtConfig()接口拿到需要的配置信息。
3. 小程序端云开发架构
小程序端云开发架构如图所示,封装一个云函数实例类供业务层调用,提供基础方法用于云环境初始化以及云调用开关的初始化,提供业务方法对云函数调用进行二次封装,比如注入监控上报逻辑,对云函数调用结果进行处理并返回。
3.1 initCloudInstance - 云环境初始化
3.1.1 云cloud实例初始化
从上一小节了解到,由第三方在批量代云开发模式下创建的云环境资源由第三方统一管理维护,第三方通过绑定环境接口将云开发资源授权给客户小程序使用,又称环境共享。因此小程序端想要使用云开发资源,需要初始化云cloud实例,如下代码所示:
客户小程序调用wx.cloud.Cloud方法,传入第三方授权给客户小程序使用的云环境 ID 以及第三方的AppID,便可获取云cloud实例,此时,客户小程序便可调用云开发 API 访问云资源了吗?
稍微了解安全原理的同学一定明白,事情不会这么简单,需要对客户小程序进行鉴权。因此,在调用方获取云cloud实例后,需要执行init方法,该方法会调用资源方的 cloudbase_auth 云函数,用于鉴权调用方的身份并制定相关权限,等待init方法执行成功后,才算真正初始化云cloud实例。
需要注意的是,小程序基础库
2.13.0及以上才支持wx.cloud.Cloud方法,对于低基础库版本,不走云调用。
3.1.2 初始化时机
云开发的独特优势在于可以免鉴权获取微信登录态,首当其冲的便是登录流程改造。众所周知,静默登录是十分前置的流程,在静默登录发起云调用之前,需要初始化云cloud实例。可推论,云cloud实例的初始化时机也是前置的。在小程序启动onLaunch生命周期中,需要调用initCloudInstance方法。
由于在原生的小程序启动流程中, App,Page,Component 的生命周期钩子函数,都不支持异步阻塞。因此可能出现在业务逻辑(比如静默登录)发起云调用时,云cloud实例还未初始化成功的情况。基于此,我们封装waitCloudInstanceInit方法,待云cloud实例初始化成功后,才发起业务层云调用。
这里我们采用Promise单例模式,抽象出一个singleInstancePromise函数供多种业务场景复用,如下代码所示,维护一个promiseInstance,当这个实例存在时,返回这个实例,否则发起CloudFunctionInstance.waitCloudInstanceInit函数调用,并消费调用结果。
那么如何保证云cloud实例只初始化一次?只需在CloudFunctionInstance类里面维护一个私有变量isInitCloudInstanceSuccess来标记云cloud实例是否初始化成功。
3.1.3 初始化耗时问题
出于安全性考虑,客户小程序云cloud实例初始化时,需要调用资源方的 cloudbase_auth 云函数进行鉴权,并获取相关权限。据我们监控上报数据统计,云cloud实例初始化的平均耗时在 400ms 左右,但会出现 1500ms-3000ms 的毛刺。
分析了一下原因,是云服务器资源分配逻辑导致,由于云函数不是始终运行的状态,而是由事件触发运行的,触发时间不可控,因此为每个云函数预留足量的实例显然会造成严重的资源浪费。为了实现对高并发的支持,云函数平台提供了「自动的弹性伸缩能力」,会在有大量请求到来时启动更多实例来处理事件请求,也会在没有事件到来时缩减函数实例甚至到零实例。SCF 平台负责所有函数运行容器的创建、管理和删除清理操作,容器启动需要一些时间,减少容器启动延迟的方法就是重用容器,即在函数调用后不对容器进行销毁,而是存留一段时间,在这段时间端内的调用会直接重用该容器。但长时间未使用就会销毁容器。
因此,当首次调用云函数、更新云函数或长时间未调用时重新调用云函数时,都需要重新启动容器(即函数的执行环境),这需要消耗一定的时间,导致毛刺的产生。
3.2 initSwitch - 初始化云函数开关配置
已知云开发资源是按量付费,需要消耗一定的成本,商家对云开发能力的需求是不同的。所以作为第三方,需要在商家端提供云函数功能开关设置服务,供商家按需开通所需的云函数功能。商家保存编辑结果后,会更新配置中心的配置信息。当用户进入商家小程序时,会请求获取配置中心数据,拿到云函数开关配置,按需调用云函数。
如上图所示,商家开通了登录、消息订阅两项云函数服务,那么用户进入商家小程序,登录、消息订阅功能会调用对应的云函数,而其他未开通功能,比如短链、二维码等等,则仍旧请求服务商提供的服务获取对应的能力。
商家端提供云函数功能开关设置服务的好处在于商家可随时切换开关状态,当一方出现故障时,可做应急处理。当配置中心的数据更新时,小程序有一定的策略及时更新配置,进而及时切换各项功能的调用方式。
如上代码所示,initSwitch方法用于初始化开关配置,需要等待配置中心数据获取成功后,才能初始化云函数的开关配置。getSwitchInitResult方法用于获取对应云函数的开关状态,从第 15 行可以看出,我们并未等待云函数开关配置初始化成功才取值。这是因为在我们的认知中,云函数的调用并不应该被获取/更新配置中心数据请求所阻塞,而是应该完全解耦。举个例子,静默登录是十分前置的流程,云开发改造静默登录流程的优势之一便是节约耗时。但是如果静默登录请求的发起依赖于该云函数的开关状态,那么得等待配置中心下发云函数开关状态成功后,才能根据开关状态发起对应的静默登录请求。实际上,如果静默登录被配置中心数据获取请求、云 cloud 初始化所阻塞,那么云开发减少的耗时的优势就会被磨平。
3.3 业务方法封装
云cloud实例初始化成功后,便可使用callFunction方法调用云函数。我们认为,将云cloud实例直接暴露给业务层调用时存在风险的,首先入参不可控,且云函数调用涉及耗时上报、监控上报、环境切换等逻辑,无形中增加了业务层代码的复杂度。因此我们在CloudFunctionInstance实例里面,进行业务方法封装,可以参考下图。
4. 登录流程改造
普通请求静默登录的目的是为了获取用户唯一标识 OpenID 和 会话密钥 session_key ,OpenID 主要用于关联生成自定义登录态、用户id、初始化用户信息数据,session_key 主要用于解密微信授权登录的手机号。云函数静默登录流程直接在云函数内使用 wx-server-sdk 提供的 getWXContext 方法获取到每次调用的上下文(appid、openid、unionid),然后传给开发者服务端。两者的区别在于,使用云函数静默登录,开发者服务端获取不到session_key。
因此,云函数静默登录和用户登录功能的开关合并成同一个登录开关,否则,假设使用云函数进行静默登录,然后使用普通请求进行授权手机号登录,开发者服务端没有存储该用户的session_key,无法解密手机号,就会导致登录失败。
云开发改造登录的流程图如上图所示,当游客首次进入小程序时,会根据配置中心开关状态判断(由于阻塞问题,新用户第一次进入小程序本地默认配置为开启)是否使用云函数发起静默登录。如果为是,那么使用云函数发起静默登录流程,如果云函数调用失败,则切换普通请求重试发起静默登录流程。注意,静默登录这里增加了二次重试机制,连续发起两次请求失败才算真正的失败。这里需要补充说明的是,云函数调用失败指调用云函数时抛出异常,而请求失败是指云函数对开发者服务端发起请求,服务端返回失败结果。静默登录请求成功后,在本地存储标识isCloudSilentLogin用于后续请求判断静默登录模式。
当游客点击授权手机号登录时,会先获取本地存储标识isCloudSilentLogin来判断静默登录模式,如果使用云函数静默登录,则调用云函数进行用户登录,如果开发者服务端返回失败结果,则等待用户重试。如果云函数调用失败,则切换成普通请求用户登录,由于此时开发者服务端并未存储session_key,需要重新发起普通请求静默登录,然后等待用户再次点击授权登录。
4.1 云函数静默登录
在微信小程序生态中,使用OpenId来标识一个用户,同一个用户的OpenId对于同一个小程序来说是永久不变的,就算用户更换手机、删除小程序,下次用户进入小程序,开发者依旧可以通过后台的记录标识出来。
所以,静默登录的目的是为了获取用户唯一标识 OpenID 和 会话密钥 session_key ,OpenID 主要用于关联生成自定义登录态、用户id、初始化用户信息数据。
改造前的静默登录流程,需要小程序端调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。服务器端调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 和 会话密钥 session_key,关联生成自定义登录态auth-token,并为每一位用户分配唯一的用户 id(uid),且初始化用户信息,然后将其返回给小程序端。
如图所示,描述云开发改造后的静默登录流程图,主要分为以下三步:
- 小程序端调用
silentLogin云函数服务。 silentLogin云函数是一段运行在云端的代码,在云函数内使用wx-server-sdk提供的getWXContext方法获取到appid、openid、unionid,将其发送给开发者服务器。- 开发者服务器获取到
openid,进行关联自定义登录态、生成业务身份(uid)、初始化用户信息等操作后将auth-token和用户信息数据返回给云函数服务,云函数服务对请求结果进行封装,返回给小程序端。
getWXContext()方法可在云函数中获取微信上下文,服务商批量代云开发模式将云环境共享给商家小程序使用,因此商家小程序调用云函数属于跨账号调用,应该获取调用方来源小程序的FROM_OPENID、FROM_UNIONID、FROM_APPID。
这里肯定有人质疑,云函数调用开发者服务端接口,直接将appid、openid、unionid作为传参,不就存在CSRF漏洞了?解决方案如下:云函数服务与开发者后端提供的服务都可以部署在 私有网络(VPC) 上,私有网络之间采用对等连接进行通信,既加快了通信速度,又提供了安全保障。小程序方调用silentLogin 云函数时,微信云函数网关将其转发到开发者自己部署的云函数服务上,运行云函数代码,与开发者后端提供的服务走对等连接进行通信,获取请求结果后返回给小程序端。
4.2 云函数用户登录
改造前的手机号授权登录流程,是小程序端将授权手机号回调返回的加密数据encryptedData和iv发送给开发者服务器,由开发者服务端使用session_key(对称解密密钥)进行对称解密,获取手机号来绑定用户id,并更新用户身份(游客->普通用户),最后将更新后的用户信息数据返回给小程序端,小程序端更新本地 storage 存储的用户信息。
如果使用云开发改造静默登录流程的话,开发者服务器是拿不到session_key的,那么如何解密手机号呢?
可以使用云调用直接获取开放数据。
云开发改造授权手机号登录流程的操作如上图所示,分为以下三步:
- 小程序端将授权手机号回调返回的
cloudID和静默登录成功后本地存储的auth-token作为参数调用userLogin云函数服务。 userLogin云函数运行代码,先云调用微信开放接口getOpenData获取云函数调用方的手机号,构造请求头部和参数,将手机号和auth-token传给开发者服务器。- 开发者服务器进行鉴权,鉴权通过后用手机号来绑定用户
id,并更新用户身份(游客->普通用户),最后将更新后的用户信息数据返回给云函数服务,云函数服务对请求结果进行封装,返回给小程序端,小程序端更新本地storage存储的用户信息。
通过我们对监控数据的观察,cloudID会出现概率性丢失的情况,所以在发起用户登录云函数调用之前,需要先判断用户授权手机号是否获取到cloudID,如为空,则终止流程,并提醒用户重试。
4.3 优化效果
云开发改造登录流程已经上线三个月,根据我们对监控数据的初略统计。改造成云开发的静默登录请求时间较改造前缩短45%以上,一次登录请求成功率也从原来的 97.78%提升到 99.98%,重试登录请求成功率从原来的 99.17%提升到 99.99%。
然而,静默登录是十分前置的流程,改造成云开发受云cloud实例初始化阻塞。想要进一步缩减耗时,需要推动微信方优化云cloud实例初始化初始化链路,比如调用资源方的 cloudbase_auth 云函数进行鉴权的流程。
普通请求用户登录需要session_key解密手机号加密数据,所以在发起请求之前,需要调用wx.checkSession判断session_key是否过期,该流程阻塞用户登录请求的发起。改造成云函数调用后,用户登录的耗时较改造前的提升并不显著,分析了一下原因,是因为云函数内需要云调用微信开放接口getOpenData,该开放接口的平均耗时在 300ms 左右。但是,由于不依赖session_key解密手机号,用户登录请求的成功率从原来的 97.32%提升到 99.99%
注意,本次数据来源于作者所在业务方近两周的监控数据,作者对数据进行粗略统计,仅供参考。
5 最后
通过对登录流程进行改造,我们验证了批量代云开发模式能够显著缩短调用链路的耗时,提升用户体验,且提供安全性保障。我们团队和微信、腾讯云达成了深度合作,作为第一批接入使用的服务商,在接入过程中发现了诸多问题,也在推动腾讯云方和微信方优化链路,反哺微信批量代云开发模式生态发展。
批量代云开发模式应用前景广泛,改造微信登录流程仅是一块验金石。我们也在尝试将团队经验梳理成最佳实践,反馈到微信服务商云开发的社区,助力服务商开发者生态发展得更好。