小程序用户登录架构设计
| updateUser
| 绑定微信授权用户信息 | 微信授权用户信息点击「允许」触发 |
| getCurrentAuthStep
| 获取当前用户登录所属阶段 | 详见下文 |
| mustAuth
| 各种触发场景拦截判断是否需要登录 | 详见下文 |
当然,session
类中还封装了一些方法用于与storage
交互,比如获取storage
中的auth-token
用于各种鉴权请求携带等等。session
类也提供的一些拓展方法,比如注销账号、解绑手机号等等用于后续需求迭代。
- 装饰器:
-
must-auth
:mustAuth
类方法的装饰器,便于业务层各种场景触发登录。 -
fuse-line
:熔断机制,如果短时间内多次调用,则停止响应一段时间,类似于 TCP 慢启动。用于解决refreshLogin
、login
等方法的并发处理问题。 -
single-queue
:单队列模式,同一时间,只允许一个正在过程中的网络请求。请求被锁定之后,同样的请求都会被推入队列,等待进行中的请求返回后,消费同一个结果。用于解决refreshLogin
、login
等方法的并发处理问题。
3.1.2 ui
- 提供通用组件供业务层调用
-
基础组件:
user-container
和phone-container
分别是获取「微信授权用户信息」和获取「微信授权手机号」的纯 UI 单元组件,给通用组件使用。 -
behavior 类:拿到授权数据后需要发送给服务端进行存储,也需要执行一些跳转逻辑判断,这些都抽象成行为类封装在
auth-flow
中,供通用组件使用。 -
通用组件:共用一个行为类,区别在于
auth-flow-container
用于页面,auth-flow-popup
用于弹窗。如下所示,小程序只有微信授权功能,则可以通过弹窗完成授权。如小程序同时提供手机号验证码和密码登录等功能,则需跳转特定登录页面。
登录流程-弹窗
3.2 libs
3.2.1 用户身份定义
用户登录阶段
综上所示,用户登录的阶段可以分为以下三步:
// 用户登录的阶段
export enum AuthStepType {
// 阶段一:游客态:静默登录成功,未绑定手机号,无用户信息
ONE = 1,
// 阶段二:会员态:用户登录成功,已绑定手机号,无用户信息
TWO = 2,
// 阶段三:会员信息态:用户登录成功,已绑定手机号,有用户信息
THREE = 3,
}
那么如何判断用户此时处于哪个步骤,基于「静默登录」的启发,原本「静默登录」成功开发者后端会将自定义登录态 auth-token
返回给前端,此处请求可以携带返回「用户信息」,同auth-token
一起命名为session
存储在本地storage
。当「用户登录」或者「更新用户信息」时,会同步更新storage
中key
为session
的数据,从而通过这些用户数据判断当前用户处于哪一个登录阶段。
以下表格列出了session
存储的部分重要的属性以及在三个阶段属性对应的值。
| 属性 | 定义 | 游客态 | 会员态 | 会员信息态 |
| — | — | — | — | — |
| authToken
| 自定义登录态 | ‘0d5bad172…’ | ‘0d5bad172…’ | ‘0d5bad172…’ |
| uid
| 用户 id | ‘001’ | ‘001’ | ‘001’ |
| busiIdentity
| 用户身份定义 | ‘VISIT’ | ‘MEMBER’ | ‘MEMBER’ |
| nickName
| 用户昵称 | ‘’ | ‘u_a1bk45’ | ‘rileycai’ |
| headUrl
| 头像链接 | ‘’ | ‘’ | ‘www.xx.com/image/…’ |
| phone
| 手机号码 | ‘’ | ‘17600888888’ | ‘17600888888’ |
| … | 其它用户信息 | … | … | … |
注意: 会员态和会员信息态的busiIdentity
值均为MEMBER
,区分会员态和会员信息态可以通过用户昵称和头像等字段,比如用户登录成功会为用户生成以’u_'开头的默认昵称和默认为空的用户头像链接。
判断用户此时处于哪个步骤的代码如下:
// 获取当前授权阶段
public getCurrentAuthStep(): AuthStepType {
// 切换账号登录的时候,始终返回AuthStepType.ONE
const loginMode = this.getLoginMode();
if (loginMode === LoginMode.SWITCH_ACCOUNT) return AuthStepType.ONE;
// 用户身份定义非会员返回AuthStepType.ONE
const userInfo = this.getUser();
if (userInfo?.busiIdentity !== ‘MEMBER’) return AuthStepType.ONE;
// 初次登录,未授权用户信息,返回AuthStepType.TWO
if (userInfo.nickName.substring(0, 2) === ‘u_’ && !userInfo.headUrl)
return AuthStepType.TWO;
// 都有,返回AuthStepType.THREE
return AuthStepType.THREE;
}
3.2.2 用户登录触发场景
前面提到过,「用户登录」的 目的是为了整合各个渠道的交易、促销、收藏等数据,针对电商小程序,目前总结的需要用户登录的场景如下所示:
用户登录场景
即当用户登录小程序时,可以正常浏览浏览商品,只有触发某些特定行为,比如领券、加购、收藏、下单等,才会判断用户是否处于登录状态,如未登录,跳转登录页面。
如下所示,封装mustAuth
方法进行拦截,未登录则跳转登录页面:
export default class Session {
…
public mustAuth({
mustAuthStep = AuthStepType.TWO, // 传人参数,需要授权的LEVEL
} = {}): Promise {
// 当前阶段处于会员态(2)或者会员信息态(3),执行resolve操作
if (this.getCurrentAuthStep() >= mustAuthStep) return Promise.resolve();
// 当前阶段处于游客态(1),跳转登录页
Navigator.gotoPage(‘/login/home’);
// 执行reject操作
return Promise.reject();
}
}
上述代码是跳转页面拦截,对于弹窗而言,需要把弹窗注入base-page
(每个页面都需要引入的通用组件,封装每个页面都需要使用的通用方法,比如错误处理等)中,通过 id 查找到弹窗组件,并进行调用。
export default class Session {
…
public mustAuth({
mustAuthStep = AuthStepType.TWO, // 需要授权的LEVEL
popupCompName = ‘auth-flow-popup’,
} = {}): Promise {
// 当前阶段处于会员态(2)或者会员信息态(3),执行resolve操作
if (this.getCurrentAuthStep() >= mustAuthStep) return Promise.resolve();
// 获取弹窗组件
const pages = getCurrentPages();
const curPage = pages[pages.length - 1];
const context = curPage.$$basePage || curPage;
const popupComp = context.selectComponent(#${popupCompName}
);
// 容错处理
if (!popupComp) {
return Promise.reject(
new Error(
“当前页面未找到 #auth-popup 组件,请参考 ‘doc/登录组件的使用方式.md’”,
),
);
}
// 调用弹窗组件方法
popupComp.setMustAuthStep(mustAuthStep);
popupComp.nextStep();
// 等待授权成功回调
return this.waitAuth();
}
}
各个业务使用时可以通过session.mustAuth().then(() => {...});
进行调用,为了提高使用体验,也可以使用装饰器@mustAuth()
来修饰各个业务需求 类的方法,装饰器源码如下:
/**
* 登录检查装饰器,使用该装饰器的方法,会先执行授权检查,如果未授权,将跳转登录页面
*/
export default function mustAuth(option = {}) {
return function(
_target: Record<string, any>,
_propertyName: string,
descriptor: TypedPropertyDescriptor<(…args: any[]) => any>,
) {
const method = descriptor.value;
descriptor.value = function(…args: any[]) {
if (!session) return;
// 登录拦截
return session.mustAuth(option).then(() => {
if (method) return method.apply(this, args);
});
};
};
}
3.3 UI
3.3.1 基础组件
1. phone-container 组件
因为需要用户主动触发才能发起获取微信授权手机号接口,需用 button
组件的点击来触发。组件代码如下所示:
// index.wxml
// index.ts
export default class PhoneContainer extends BaseComponent {
getPhoneNumber(
e: WechatMiniprogram.Event<WechatMiniprogram.GetPhoneNumberCallbackResult>,
) {
this.triggerEvent(‘getphonenumber’, { …e.detail, authType: AuthType.PHONE,});
}
}
phone-container
是一个纯 UI 组件,通过triggerEvent
事件将获取手机号数据传递给父组件,
2. user-container 组件
user-container
组件是获取微信授权用户信息的纯 UI 组件,之前通过<button open-type="getUserInfo" bindgetUserInfo="getUserInfo"/>
的方式进行获取。2021 年 2 月 23 日,微信团队发布了《小程序登录、用户信息相关接口调整说明》,新增getUserProfile
接口替代原来的wx.getUserInfo
,来获取用户头像、昵称、性别及地区信息,也是通过button
组件的点击来触发。两者的区别如下图所示:
获取用户信息接口区别
2012 年 4 月 13 日之前,使用wx.getUserInfo
弹出授权弹窗时,如果用户点击允许授权,那么会记录用户的行为,下次再点击时,不会弹窗而是直接将授权结果返回。4 月 13 日之后后,使用wx.getUserProfile
,开发者每次通过该接口获取用户个人信息均需用户确认,因此需要妥善保管用户授权的头像昵称,避免重复弹窗。
3.3.2 行为类
如下图所示,auth-flow
行为类主要封装用户、小程序、服务端三者之间的交互逻辑。
用户行为
在「微信授权登录」过程中,小程序拿到加密的encryptedData
和iv
数据,将其和携带的auth-token
一起发送给开发者服务器,服务端通过auth-token
鉴权识别这个用户,并使用静默登录成功获取的session_key
(对称解密密钥)对encryptedData
和iv
数据进行对称解密,获取该用户的手机号,将手机号与uid
绑定,此时该用户成功注册会员,并将会员信息返回给小程序端。
小程序端更新本地storage
存储的session
数据,此时busiIdentity
的值已经从VISIT
更新为MEMBER
,用户身份转变为会员态,登录成功。
在「授权用户信息」的过程中,小程序调用wx.getUserProfile
方法拿到用户数据,并将这些数据与携带的auth-token
一起发送给开发者服务器,服务端通过auth-token
鉴权识别这个用户,更新该用户的信息并将新的会员数据返回给小程序端。
小程序端更新本地storage
存储的session
数据,此时用户昵称和头像均已更新,用户身份转变为会员信息态,授权成功。
眼尖的读者一定观察到了,时序图中还对微信头像做了转存。这是因为用户在微信端修改微信头像后,之前「授权用户信息」获取的微信头像链接就会失效,因此开发者应该在自己获取用户信息后,将头像保存下来,避免微信头像 URL 失效后的异常情况。
3.3.3 通用组件
通用组件是对基础组件和行为类的二次封装,主要是为业务层提供弹窗登录和页面登录两种能力。
4. 总结
我们将用户登录能力从业务层中抽象出来,统一封装在service
层,便于复用。本文主要讲述的是service
层的架构,对于业务层的逻辑实现并没有多加累赘。下列表格以小程序端为例,简述了「静默登录」和「用户登录」整套方案的前后端逻辑实现。
| 业务场景 | 用户感知 | 前端处理逻辑 | 后端处理逻辑 | 补充说明 |
| — | — | — | — | — |
| 扫码搜索等各种方式进入小程序 | 无 | 1、判断:当前小程序是否缓存了登录态auth-token
且使用wx.checkSeesion
检查当前用户在小程序中登录态是否过期,过期执行步骤 2; |
|
|
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(资料价值较高,非无偿)
最后
技术是没有终点的,也是学不完的,最重要的是活着、不秃。零基础入门的时候看书还是看视频,我觉得成年人,何必做选择题呢,两个都要。喜欢看书就看书,喜欢看视频就看视频。最重要的是在自学的过程中,一定不要眼高手低,要实战,把学到的技术投入到项目当中,解决问题,之后进一步锤炼自己的技术。
技术学到手后,就要开始准备面试了,找工作的时候一定要好好准备简历,毕竟简历是找工作的敲门砖,还有就是要多做面试题,复习巩固。有需要面试题资料的朋友点击这里即可获取!!!。
11768631417)]
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(资料价值较高,非无偿)
最后
技术是没有终点的,也是学不完的,最重要的是活着、不秃。零基础入门的时候看书还是看视频,我觉得成年人,何必做选择题呢,两个都要。喜欢看书就看书,喜欢看视频就看视频。最重要的是在自学的过程中,一定不要眼高手低,要实战,把学到的技术投入到项目当中,解决问题,之后进一步锤炼自己的技术。
技术学到手后,就要开始准备面试了,找工作的时候一定要好好准备简历,毕竟简历是找工作的敲门砖,还有就是要多做面试题,复习巩固。有需要面试题资料的朋友点击这里即可获取!!!。
[外链图片转存中…(img-vl1q3ZOT-1711768631418)]