从121我的页面分析小程序登录和授权

2022年03月20日 2,044次浏览

曹顺航(无月)

1.背景

目前APP+小程序的轻应用模式已经成为一种流行趋势,海拍客为了进驻toc市场,提升门店线上卖货的能力,推出了121小程序,目前在如火如荼的进行中。

说到小程序,一定绕不开的就是登录和授权, 好的登录授权流程和容错机制能够帮助小程序维持好的用户体系和保证功能的正常使用。

虽然看官方文档感觉就几个api很简单,但是为了保证121能够稳定的运行,避免因登录授权问题导致应用的阻塞,需要对这个流程好好打磨一下。

针对我在我的页面在静默登录、用户登录、用户信息授权相关开发流程中出现的问题和解决方法,以及一些个人思考,分享下小程序登录授权相关的内容。

2.流程

我的页面登录授权简易流程如下图

接下来根据流程图两个主要步骤,静默登录、用户登录和用户信息授权分别来分析

3.静默登录

3.1什么是静默登录?

通过小程序微信官方提供的登录能力、api,配合服务端提供的登录接口,在用户不授权,无感知的情况下完成登录。快速建立小程序的用户体系。按照官方文档的说明,结合121登录流程可以简单概括为三步:

1.init或者因为接口判定未登录情况下,调用wx.login()获取临时登录凭证code,作为入参调用121登录接口并回传到开发者服务器。

2.服务器端调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 和 会话密钥 session_key(用于获取微信开放数据)。

3.小程序把接口返回的token、userId等数据存入storage,作为以后调用接口的凭证,帮助业务逻辑中前后端交互时识别用户身份。

4.每次请求会在请求数据里面带上token值,服务端拿到后会调用usecenter相关接口解析token,识别用户身份。

3.2静默登录流程图

微信官方提供了微信登录流程时序图,图中红色圈出部分就属于静默登录:

3.3静默登录开发中遇到的问题

3.3.1session_key的时效性问题的解决

根据微信官方说法,session_key可能因为长时间(用户越久未使用小程序,登录状态越有可能失效)或者wx.login()的调用而使旧的session_key失效,导致调用接口失败。同时微信是不会把session_key的有效期告诉开发者。

官方给出的解决方案,可以通过wx.checkSession在调用接口前检查登录状态是否有效。但踩过几次坑后我发现,wx.checkSession的准确性并不是100%,有时在session_key过期的情况下也会返回true。

查阅相关资料和微信社区讨论后,发现这个问题还是一直存在。所以需要对session_key做一些容错处理。

121的解决方案是在后端使用session_key解密开放数据失败后,返回特定错误码('NO_LOGIN_SESSION'),重新走一遍静默登录刷新登录状态。再刷新页面,代码简化如下

async silentLogin() {
     // 获取code
     const code = await wxLogin();
     // 调用服务端接口,发送code
     const res = await API.login(code);
     // 保存数据到storage
     wx.setStorageSync('xxx', res.data)
}

if (json.message === 'NO_LOGIN_SESSION') {
      // 静默登录
      await silentLogin();
      // 刷新页面
     ...
      page.onLoad(page.options);
      page.onShow();
}

3.3.2login接口大量重复调用问题解决

121在线上运行一段时间后,用户端组反映小程序有重复请求登录接口的情况发生,甚至有同一页面重复调用的情况,导致后台一直有告警日志。排查后,发现主要原因如下:

  1. session_key的问题解决方案导致每次请求返回'NO_LOGIN_SESSION'都会重新走登录流程。
  2. 一些业务场景复杂的页面调用的接口比较多,比如商详页。而且这些接口没有整理调用顺序和依赖关系,有可能出现并行调用的情况。

解决方案如下:

  1. 对121通用登录组件的autoLogin方法进行节流,在一个登录周期没有完成之前,下一个登录调用会被return回去。
    因为code不会过期的这么快,所以即便短时间内有大量登录函数调用,也只会执行一次,不会影响功能。代码简化如下:
/** 是否开始登录的标记 */
const isStartedLogin: boolean = false;

function autoLogin() {
    if(isStartedLogin) return false;
    // 状态置为true
    isStartedLogin = true;
    // 执行登录相关内容
    ...
    login().finally(() => {
        // 登录流程结束后置为false
        isStartedLogin = false;
    })
}

2.对业务逻辑复杂页面各个接口调用进行整理,梳理接口调用前后顺序,形成一个链式关系。对入口接口的调用做监听,返回未登录code时直接停止后续加载流程。
优化过后重复登录告警的问题基本上没有出现过了

4.用户登录

4.1 什么是用户登录?

静默登录是用户无感知的,不需要用户主动触发的一种登录行为。通过它可以快速获取用户身份标识,快速建立小程序的用户体系。

但是对于121来说,业务场景更复杂,对于不同身份的用户提供了不同的信息展示和操作权限等等(例如导购和店长可以在我的页面看到收益模块,店长可以在我的页面管理导购)。因此在静默登录的基础上,需要用户主动触发门店登录。

4.2 121用户身份

用户门店登录除了常规的账号密码/手机登录外,还需要加一层绑定用户关系的步骤,简化代码如下:

// 账号密码登录
data.userNickName && password(data, {
       customFail: true
}).then(res => {
       bindStaffUser({ loginToken: String(res.data.code) }).then(() => {
            wx.reLaunch({
                  url: '/pages/index/index'
               });
            });
}).catch((res: any) => {// 登录失败的处理}

门店登录后身份的转变关系如下图

4.3 121用户登录常用场景

用户登录主要作用就是身份区分,因为不同的场景和操作需要不同身份的用户,如图是121部分身份差异化的权限。

5. 用户授权

121我的页面需要获取用户的头像和昵称等信息,这些信息对于用户来说是隐私的。所以需要经过用户授权通过后才能调用接口。

5.1 获取信息接口

wx.getUserInfo(Object object)
旧的获取用户信息接口。海拍客之前的小程序通过该api获取用户信息,但是微信发布了公告,2021年4月28日24时后发布的小程序,无法通过该接口获取用户信息,直接获取匿名数据。
原因是用户可能会误操作点了拒绝授权,但是再次点击后无法继续唤起授权弹窗,导致小程序正常功能无法使用,不利于121维系用户。

wx.getUserProfile(Object object)
官方推荐最新获取用户信息的接口,目前121已经替换为该接口。需要配合页面的点击事件一起使用,每次请求都会弹出授权窗口。用户同意后即可返回用户信息。
主要获取的数据如下,用于传给用户组保存用户信息

属性说明
encryptedData包括敏感数据在内的完整用户信息的加密数据
iv加密算法的初始向量

5.2 手机号授权code时效只有5分钟解决

由于一些业务场景的需要,需要我们获取用户手机号。手机号作为用户隐私信息,需要用户主动点击按钮触发授权获,同意后前端拿到code调用接口传给服务端以用来换取手机号和session_key来做绑定。
具体流程如下图:

从图中可以看出,调用绑定接口需要wx.login的code(登录凭证)和bindgetphonenumber事件的code(动态令牌)这两个参数,但是这两个code时效都只有5分钟。
所以怎么保证用户手机授权的时候能够同时获取这两个code且都是有效的呢?
以121商品详情页手机授权为例:

1.进入商详页首先调用wx.login接口获取code存储到当前页面状态里,商详加载过程中调用currentUser接口判断当前用户是否已经手机授权。已经授权就正常加载下单按钮,如果没有前端会给下单按钮套一层手机授权按钮的壳。

2.用户点击下单的时候会优先弹出手机授权窗口,用户同意且绑定成功后才会正常走下单流程。

3.点击同意后会拿到手机号的动态令牌,接口之前的登录凭证,一起调用接口给服务端去解析。

4.如果登录凭证过期,服务端返回特定码告诉前端,前端再次调用wx.login获取新的code来替换老的code。

5.3 获取用户信息服务端解密失败问题解决

服务端在用code去获取session_key,再去解密encryptedData,会出现签名失败的错误。
原因是调用顺序问题,wx.login必须在wx.getUserProfile之前调用,这样解密encryptedData才会成功。为了解决这个问题,前端这边想了两个解决方案:

  1. 在wx.login成功的回调里获取code,再去调用wx.getUserProfile,希望通过这样来保证获取code和encryptedData顺序。代码如下:
wx.login({
  success (res) {
    wx.getUserProfile({
       success: (res) => {
          // 调用服务端接口
          authByPersonal()  
       }
    })
  }
})

但是实际操作中行不通(getUserProfile:fail can only be invoked by user TAP gesture),因为如上文所说,wx.getUserProfile需要配合点击事件才能触发。

  1. 那么只能换一种思路去思考,这时候就想到了Promise, 虽然Promise.all是并行执行,但是输出是按照顺序的。可以通过Promise来保证执行顺序。具体实现步骤如下:

    第一步,wx.getUserProfile和wx.login封装了成两个单独的promise函数:

  // 获取用户信息  
  wxGetUserProfile: function () {
        return new Promise((resolve, reject) => {
            wx.getUserProfile({
                lang: 'zh_CN',
                desc: '用于完善用户资料', 
                success: (res) => {
                    resolve(res);
                },
                // 失败回调
                fail: (err) => {
                    reject(err);
                }
            });
        });
    },
    // wx.login
    wxSilentLogin: function () {
        return new Promise((resolve, reject) => {
            wx.login({
                success(res) {
                    resolve(res.code);
                },
                fail(err) {
                    reject(err);
                }
            });
        });
    }

第二步,在头像上绑定事件

<image class="avatar"bindtap="handleGetUserInfo" />

第三步,使用Promise.all来保证顺序性

handleGetUserInfo() {
        const _this = this;
        let p1 = this.wxSilentLogin();
        let p2 = this.wxGetUserProfile();
        Promise.all([p1, p2]).then((res: any) => {
            // 首先获取code
            let code = res[0];
            // 再获取加密用户信息和加密算法初始量
            let iv = res[1].iv;
            let encryptData = res[1].encryptedData;
            // 所有参数请求服务器,保存用户数据
            authByPersonal({
                appCode: 'wxd657e9972b040294',
                encryptData,
                iv,
                jsCode: code
            }).then(() => {
                // 刷新121我的页面数据
                _this.getData();
            });
        }).catch((err) => {
            console.log(err);
        });
    },

通过这样的优化,服务端再去解密encryptedData就没有出现签名失败的错误了。

5.4 用户头像和昵称无法更新和头像加载失败问题解决

原因是海拍客之前的小程序授权信息是互通的,只要用户再其他小程序保存过用户信息,即便第一次使用121,也会默认是已经授权过了的。

这样造成的问题是,在其他小程序授权过的用户调用数据接口会返回默认头像和昵称,有时头像甚至返回null。咨询过服务端后,给出的结论是需要从底层改造,需要大量时间和人力投入。所以只能前端这边给出解决方案。

  1. 对于头像加载失败的问题,首先提出UI给出一张错误头像的兜底图,然后在错误情况出现时强行把头像换成兜底图,避免页面留白或者样式错位。第一种错误情况是获取头像为null时,第二种是监听image的binderror(表示当年图片地址加载图片失败)事件,在回调中替换。具体可以参考第一张流程图。
// 替换头像函数
handleImgError() {
    this.setData({
        avatarUrl: 'xxx'
    });
}
// 头像为null时
myPageInfo({}).then((data) => {
    // 头像返回null给个兜底图
    if (!data.avatarUrl === null) {
        this.handleImgError();
    }
})

// 监听错误
<image binderror="handleImgError" src="{{avatarUrl}}" />

  1. 新增交互,在默认头像出现后,用户点击头像也可以重新走一遍用户信息授权来更新头像和昵称。

6.总结

6.1 登录相关

前面介绍了静默登录和用户登录两种方式,现在总结下两者的区别

登录方式静默登录用户登录
作用帮助业务快速建立用户体系,后续前后端交互时识别用户身份绑定门店账号,确认用户身份
用户是否感知无感知,小程序后台执行需用用户手动触发,走完登录流程
调用时机一般小程序启动时调用,由于页面和组件生命周期都不支持异步阻塞,所以可能会出现wx.login还没有成功,接口就已经调用,最后导致报错。所以需要对这种情况做兼容处理,支持异步,开发者自己定义交互逻辑
登录失效session_key会存在失效的情况登录失效直接返回登录页面重新登录了
必要性几乎所有小程序都需要静默登录非必要,看业务复杂度需要

6.2 授权相关

本文主要讲了用户信息获取授权相关的内容,小程序还有很多其他授权,例如位置授权、小相册授权、摄像头授权等等。总结下来,关于授权需要注意一下三点:

  • 授权都需要用用户主观上主动触发,所以需要做好引导
  • 要做好授权不成功的兜底处理
  • 对于需要频繁改变的授权信息,需要给用户二次授权的机会

关于登录和授权先写到这里,具体实现还是得根据不同业务来做修改,感谢收看~

参考文档