La raison pour laquelle la conception de la connexion/inscription est si soigneusement conçue est bien sûr de rendre cette fonctionnalité de base de l'application suffisamment robuste pour éviter l'apparition de l'ensemble. site. obstruction sexuelle.
Dans le même temps, nous devons réfléchir pleinement à la manière de découpler et d'encapsuler lors du développement de nouveaux petits programmes, nous pouvons rapidement supprimer les capacités de réutilisation et éviter les pièges répétés.
Le module de connexion et d'inscription est comme un iceberg. Nous pensons qu'il suffit de "entrer votre compte et votre mot de passe, et vous êtes connecté". Mais en fait, il y a plusieurs problèmes à prendre en compte.
Ici, j'aimerais partager avec vous toutes les expériences et idées de conception que j'ai accumulées après avoir récemment terminé un petit module de connexion/inscription au programme.
Dans le processus de navigation des utilisateurs dans le mini-programme, en raison des besoins professionnels, il est souvent nécessaire d'obtenir certaines informations de base sur l'utilisateur :
, etc., vous besoin de demander l'autorisation de l'utilisateur. unionId
Enregistrement d'un compte et d'un mot de passe
Au début d'Internet, les informations personnelles La couverture des e-mails et des téléphones portables est limitée. Par conséquent, l'utilisateur doit penser à un nom de compte. Nous enregistrons un compte QQ, qui est ce formulaire.Enregistrement d'une adresse e-mail
Après le millénaire, l'ère Internet des PC est rapidement devenue populaire , et nous avons tous créé Obtenez votre propre e-mail personnel. De plus, QQ est également livré avec un compte de messagerie. Étant donné que l'e-mail est privé et peut communiquer des informations, la plupart des sites Web ont commencé à utiliser des comptes de messagerie comme noms d'utilisateur pour l'enregistrement, et devront se connecter à l'e-mail correspondant pour vérifier les e-mails d'activation pendant le processus d'inscription afin de vérifier que nous en sommes propriétaires. l'adresse e-mail enregistrée.Enregistrement du numéro de téléphone portable
Après la popularité d'Internet, les smartphones et l'Internet mobile se sont développés rapidement . Les téléphones mobiles sont devenus un appareil mobile indispensable pour chacun, et l'Internet mobile est profondément intégré dans la vie moderne de chacun. Par conséquent, par rapport au courrier électronique, les numéros de téléphone mobile sont actuellement plus étroitement liés aux individus, et de plus en plus d'applications mobiles apparaissent, et les méthodes d'enregistrement qui utilisent les numéros de téléphone mobile comme noms d'utilisateur sont également largement utilisées.En 2020, le nombre d'utilisateurs de WeChat a atteint 1,2 milliard. Eh bien, les comptes WeChat, du moins en Chine, sont devenus la « marque d'identité » de la nouvelle génération du monde Internet.
Pour l'applet WeChat, il est naturel de connaître l'ID de compte WeChat de l'utilisateur actuel. WeChat permet aux applications de mini-programmes de se « connecter » silencieusement à nos applications de mini-programmes sans que l'utilisateur s'en rende compte. C'est ce que nous appelons souvent « connexion silencieuse ».
En fait, la connexion de l'applet WeChat est essentiellement le même concept que « l'authentification unique » des applications Web traditionnelles.
Étant donné que HTTP est à l'origine sans état, l'approche générale de base de l'industrie en matière de statut de connexion est la suivante :
Dans l'applet WeChat, la "couche logique JS" n'est pas un environnement de navigateur, donc naturellement il n'y en a pasCookie
, donc généralement Utilisera la méthode access token
.
Pour les produits qui doivent obtenir davantage de surnoms d'utilisateurs, de numéros de téléphone portable d'utilisateurs et d'autres informations. Dans un souci de confidentialité des utilisateurs, WeChat exige que les utilisateurs acceptent activement d'autoriser. Seules les applications de mini-programmes peuvent obtenir cette partie des informations. Il s'agit de l'interaction des mini-programmes actuellement populaires « informations sur l'utilisateur autorisé » et « numéro de téléphone mobile autorisé ».
En raison des différentes sensibilités des différentes informations utilisateur, les mini-programmes WeChat fournissent une « autorisation » pour différentes informations utilisateur de différentes manières :
wx.getLocation()
, si l'utilisateur n'est pas autorisé, l'interface d'autorisation d'adresse apparaîtra. wx.getLocation()
reviendra directement à l'échec. <button open-type="xxx"></button>
manière. wx.authorize()
pour demander une autorisation à l'avance, et vous n'avez pas besoin de réafficher l'autorisation lorsque vous avez besoin d'obtenir des informations pertinentes plus tard. Après avoir trié le concept, notre module peut être divisé en deux parties principales :
Session
Auth
3.1.1 Connexion silencieuse
wx.login()
openId
session_key
Le backend transmet ce code au serveur WeChat en échange de l'identifiant unique de l'utilisateur Si vous mettez simplement en œuvre ce processus, c'est assez simple.
wx.login()
Réduire
:wx.login()
session_key
Parce que session.login()
produira des effets secondaires imprévisibles, par exemple, cela peut rendre wx.login()
invalide, entraînant l'échec des scénarios de décryptage d'autorisation ultérieurs. Ici, nous pouvons fournir une méthode comme
Moment de l'appel
: app.onLaunch()
Bien sûr, vous pouvez également lancer un appel de connexion de manière bloquante asynchrone lorsque la première interface nécessitant un statut de connexion est appelée. Cela nécessite une couche d'interface bien conçue.
Les idées de conception détaillées pour les deux scénarios mentionnés ci-dessus seront discutées ci-dessous.
Problèmes avec les appels simultanés :
Dans les scénarios professionnels, il est inévitable que plusieurs codes doivent déclencher la connexion si vous. face à des situations extrêmes, ces multiples codes lancent des appels en même temps. Cela entraînera le lancement du processus de connexion plusieurs fois dans un court laps de temps, même si la demande précédente n'a pas été complétée. Dans cette situation, nous pouvons bloquer le premier appel et attendre le résultat des appels suivants, tout comme le processus d’union des spermatozoïdes et des ovules.
Problème avec les appels non expirés :
Si notre statut de connexion n'a pas expiré, il peut être utilisé normalement par défaut, ce n'est pas nécessaire. pour rappeler. Allez lancer le processus de connexion. À ce stade, nous pouvons d'abord vérifier si l'état de connexion est disponible par défaut. Sinon, nous pouvons lancer une demande. Ensuite, vous pouvez également fournir un paramètre similaire à session.login({ force: true })
pour forcer la connexion.
1 Appelé au démarrage de l'application
Car dans la plupart des cas. Ils doivent tous s'appuyer sur l'état de connexion. On pensera naturellement à appeler cet appel au démarrage de l'application (app.onLaunch()
).
Cependant, en raison du processus de démarrage natif de l'applet, les fonctions de hook de cycle de vie de App
, Page
et Component
ne prennent pas en charge le blocage asynchrone.
Ensuite, nous pouvons facilement constater que le "processus de connexion" initié par app.onLaunch
n'est pas terminé au moment de page.onLoad
, et nous ne pourrons pas effectuer correctement certaines opérations qui dépendent de la connexion État.
Pour cette situation, nous avons conçu une machine-outil d'état : status
Basé sur la machine d'état, nous pouvons écrire du code comme celui-ci :
import { Status } from '@beautywe/plugin-status';// on app.jsApp({ status: { login: new Status('login'); }, onLaunch() { session // 发起静默登录调用 .login() // 把Comprendre la conception frontale et la mise en œuvre de la connexion au mini-programme WeChat设置为 success .then(() => this.status.login.success()) // 把Comprendre la conception frontale et la mise en œuvre de la connexion au mini-programme WeChat设置为 fail .catch(() => this.status.login.fail()); }, });// on page.jsPage({ onLoad() { const loginStatus = getApp().status.login; // must 里面会进行状态的判断,例如登录中就等待,登录成功就直接返回,登录失败抛出等。 loginStatus().status.login.must(() => { // 进行一些需要登录态的操作... }); }, });复制代码
2. Initiez la connexion lorsque la "première interface nécessitant un état de connexion" est appelée
De plus, nous constaterons qu'un niveau plus profond d'état de connexion est requis. Le nœud est lorsque le " L'API backend qui nécessite un statut de connexion" est lancée.
Ensuite, nous pouvons lancer une "connexion silencieuse" lors de l'appel d'une "API backend qui nécessite un statut de connexion". Pour les scénarios simultanés, laissez simplement les autres requêtes attendre.
En utilisant fly.js comme "couche de requête réseau" encapsulée par wx.request()
, faites un exemple simple :
// 发起请求,并表明该请求是需要登录态的fly.post('https://...', params, { needLogin: true });// 在 fly 拦截器中处理逻辑fly.interceptors.request.use(async (req)=>{ // 在请求需要登录态的时候 if (req.needLogin !== false) { // ensureLogin 核心逻辑是:判断是否已登录,如否发起登录调用,如果正在登录,则进入队列等待回调。 await session.ensureLogin(); // 登录成功后,获取 token,通过 headers 传递给后端。 const token = await session.getToken(); Object.assign(req.headers, { [AUTH_KEY_NAME]: token }); } return req; });复制代码
Lorsque le statut de connexion personnalisé expire, le backend doit renvoyer un code de statut spécifique, tel que : AUTH_EXPIRED
, AUTH_INVALID
, etc.
Le front-end peut surveiller le code d'état de toutes les requêtes dans la "couche de requêtes réseau", puis lancer une actualisation de l'état de connexion, puis rejouer la requête ayant échoué :
// 添加响应拦截器fly.interceptors.response.use( (response) => { const code = res.data; // 登录态过期或失效 if ( ['AUTH_EXPIRED', 'AUTH_INVALID'].includes(code) ) { // 刷新登录态 await session.refreshLogin(); // 然后重新发起请求 return fly.request(request); } } )复制代码
Ensuite, si il existe de nombreuses requêtes simultanées. Chaque requête renvoie un code d'état indiquant que l'état de connexion n'est pas valide, et le code ci-dessus sera exécuté plusieurs fois.
Nous devons effectuer un traitement spécial de tolérance aux pannes pour session.refreshLogin()
:
Exemple de code :
class Session { // .... // 刷新登录保险丝,最多重复 3 次,然后熔断,5s 后恢复 refreshLoginFuseLine = REFRESH_LOGIN_FUSELINE_DEFAULT; refreshLoginFuseLocked = false; refreshLoginFuseRestoreTime = 5000; // 熔断控制 refreshLoginFuse(): Promise<void> { if (this.refreshLoginFuseLocked) { return Promise.reject('刷新登录-保险丝已熔断,请稍后'); } if (this.refreshLoginFuseLine > 0) { this.refreshLoginFuseLine = this.refreshLoginFuseLine - 1; return Promise.resolve(); } else { this.refreshLoginFuseLocked = true; setTimeout(() => { this.refreshLoginFuseLocked = false; this.refreshLoginFuseLine = REFRESH_LOGIN_FUSELINE_DEFAULT; logger.info('刷新登录-保险丝熔断解除'); }, this.refreshLoginFuseRestoreTime); return Promise.reject('刷新登录-保险丝熔断!!'); } } // 并发回调队列 refreshLoginQueueMaxLength = 100; refreshLoginQueue: any[] = []; refreshLoginLocked = false; // 刷新登录态 refreshLogin(): Promise<void> { return Promise.resolve() // 回调队列 + 熔断 控制 .then(() => this.refreshLoginFuse()) .then(() => { if (this.refreshLoginLocked) { const maxLength = this.refreshLoginQueueMaxLength; if (this.refreshLoginQueue.length >= maxLength) { return Promise.reject(`refreshLoginQueue 超出容量:${maxLength}`); } return new Promise((resolve, reject) => { this.refreshLoginQueue.push([resolve, reject]); }); } this.refreshLoginLocked = true; }) // 通过前置控制之后,发起登录过程 .then(() => { this.clearSession(); wx.showLoading({ title: '刷新登录态中', mask: true }); return this.login() .then(() => { wx.hideLoading(); wx.showToast({ icon: 'none', title: '登录成功' }); this.refreshLoginQueue.forEach(([resolve]) => resolve()); this.refreshLoginLocked = false; }) .catch(err => { wx.hideLoading(); wx.showToast({ icon: 'none', title: '登录失败' }); this.refreshLoginQueue.forEach(([, reject]) => reject()); this.refreshLoginLocked = false; throw err; }); }); // ...}复制代码</void></void>
Après avoir effectué la « connexion silencieuse » ci-dessus, le serveur WeChat le problème A session_key
est destiné au backend, et il sera utilisé lorsque vous aurez besoin d'obtenir des données ouvertes WeChat.
Et session_key
est sensible au temps. Ce qui suit est tiré de la description officielle de WeChat :
Validité de la clé de session session_key
Si les développeurs rencontrent un échec de vérification de signature ou un échec de déchiffrement en raison d'une session_key incorrecte, veuillez prêter attention aux notes suivantes liées à session_key.
- wx.login 调用时,用户的 session_key 可能会被更新而致使旧 session_key 失效(刷新机制存在最短周期,如果同一个用户短时间内多次调用 wx.login,并非每次调用都导致 session_key 刷新)。开发者应该在明确需要重新登录时才调用 wx.login,及时通过 auth.code2Session 接口更新服务器存储的 session_key。
- 微信不会把 session_key 的有效期告知开发者。我们会根据用户使用小程序的行为对 session_key 进行续期。用户越频繁使用小程序,session_key 有效期越长。
- 开发者在 session_key 失效时,可以通过重新执行登录流程获取有效的 session_key。使用接口 wx.checkSession可以校验 session_key 是否有效,从而避免小程序反复执行登录流程。
- 当开发者在实现自定义登录态时,可以考虑以 session_key 有效期作为自身登录态有效期,也可以实现自定义的时效性策略。
翻译成简单的两句话:
session_key
时效性由微信控制,开发者不可预测。wx.login
可能会导致 session_key
过期,可以在使用接口之前用 wx.checkSession
检查一下。而对于第二点,我们通过实验发现,偶发性的在 session_key
已过期的情况下,wx.checkSession
会概率性返回 true
社区也有相关的反馈未得到解决:
所以结论是:wx.checkSession
可靠性是不达 100% 的。
基于以上,我们需要对 session_key
的过期做一些容错处理:
session_key
的请求前,做一次 wx.checkSession
操作,如果失败了刷新登录态。session_key
解密开放数据失败之后,返回特定错误码(如:DECRYPT_WX_OPEN_DATA_FAIL
),前端刷新登录态。示例代码:
// 定义检查 session_key 有效性的操作const ensureSessionKey = async () => { const hasSession = await new Promise(resolve => { wx.checkSession({ success: () => resolve(true), fail: () => resolve(false), }); }); if (!hasSession) { logger.info('sessionKey 已过期,刷新登录态'); // 接上面提到的刷新登录逻辑 return session.refreshLogin(); } return Promise.resolve(); }// 在发起请求的时候,先做一次确保 session_key 最新的操作(以 fly.js 作为网络请求层为例)const updatePhone = async (params) => { await ensureSessionKey(); const res = await fly.post('https://xxx', params); }// 添加响应拦截器, 监听网络请求返回fly.interceptors.response.use( (response) => { const code = res.data; // 登录态过期或失效 if ( ['DECRYPT_WX_OPEN_DATA_FAIL'].includes(code)) { // 刷新登录态 await session.refreshLogin(); // 由于加密场景的加密数据由用户点击产生,session_key 可能已经更改,需要用户重新点击一遍。 wx.showToast({ title: '网络出小差了,请稍后重试', icon: 'none' }); } } )复制代码
在用户信息和手机号获取的方式上,微信是以 <button open-type="'xxx'"></button>
的方式,让用户主动点击授权的。
那么为了让代码更解耦,我们设计这样三个组件:
<user-contaienr getuserinfo="onUserInfoAuth"></user-contaienr>
: 包装点击交互,通过 <slot></slot>
支持点击区域的自定义UI。<phone-container getphonennmber="onPhoneAuth"></phone-container>
: 与 <user-container></user-container>
同理。<auth-flow></auth-flow>
: 根据业务需要,组合 <user-container></user-container>
、<phone-container></phone-container>
组合来定义不同的授权流程。以开头的业务场景的流程为例,它有这样的要求:
那么授权的阶段可以分三层:
// 用户登录的阶段export enum AuthStep { // 阶段一:只有登录态,没有用户信息,没有手机号 ONE = 1, // 阶段二:有用户信息,没有手机号 TWO = 2, // 阶段三:有用户信息,有手机号 THREE = 3, }复制代码
AuthStep
的推进过程是不可逆的,我们可以定义一个 nextStep
函数来封装 AuthStep 更新的逻辑。外部使用的话,只要无脑调用 nextStep
方法,等待回调结果就行。
示例伪代码:
// auth-flow componentComponent({ // ... data: { // 默认情况下,只需要到达阶段二。 mustAuthStep: AuthStep.TWO }, // 允许临时更改组件的需要达到的阶段。 setMustAuthStep(mustAuthStep: AuthStep) { this.setData({ mustAuthStep }); }, // 根据用户当前的信息,计算用户处在授权的阶段 getAuthStep() { let currAuthStep; // 没有用户信息,尚在第一步 if (!session.hasUser() || !session.hasUnionId()) { currAuthStep = AuthStepType.ONE; } // 没有手机号,尚在第二步 if (!session.hasPhone()) { currAuthStep = AuthStepType.TWO; } // 都有,尚在第三步 currAuthStep = AuthStepType.THREE; return currAuthStep; } // 发起下一步授权,如果都已经完成,就直接返回成功。 nextStep(e) { const { mustAuthStep } = this.data; const currAuthStep = this.updateAuthStep(); // 已完成授权 if (currAuthStep >= mustAuthStep || currAuthStep === AuthStepType.THREE) { // 更新全局的授权Comprendre la conception frontale et la mise en œuvre de la connexion au mini-programme WeChat,广播消息给订阅者。 return getApp().status.auth.success(); } // 第一步:更新用户信息 if (currAuthStep === AuthStepType.ONE) { // 已有密文信息,更新用户信息 if (e) session.updateUser(e); // 更新到视图层,展示对应UI,等待获取用户信息 else this.setData({ currAuthStep }); return; } // 第二步:更新手机信息 if (currAuthStep === AuthStepType.TWO) { // 已有密文信息,更新手机号 if (e) this.bindPhone(e); // 未有密文信息,弹出获取窗口 else this.setData({ currAuthStep }); return; } console.warn('auth.nextStep 错误', { currAuthStep, mustAuthStep }); }, // ...});复制代码
那么我们的 <auth-flow></auth-flow>
中就可以根据 currAuthStep
和 mustAuthStep
来去做不同的 UI 展示。需要注意的是使用 <user-container></user-container>
、<phone-container></phone-container>
的时候连接上 nextStep(e)
函数。
示例伪代码:
<view> <!-- 已完成授权 --> <block> <view>已完成授权</view> </block> <!-- 未完成授权,第一步:Comprendre la conception frontale et la mise en œuvre de la connexion au mini-programme WeChat --> <block> <user-container> <view>Comprendre la conception frontale et la mise en œuvre de la connexion au mini-programme WeChat</view> </user-container> </block> <!-- 未完成授权,第二步:Comprendre la conception frontale et la mise en œuvre de la connexion au mini-programme WeChat --> <block> <phone-container> <view>Comprendre la conception frontale et la mise en œuvre de la connexion au mini-programme WeChat</view> </phone-container> </block> </view>复制代码
到这里,我们制作好了用来承载授权流程的组件 <auth-flow></auth-flow>
,那么接下来就是决定要使用它的时机了。
我们梳理需要授权的场景:
点击某个按钮,例如:购买某个商品。
对于这种场景,常见的是通过弹窗完成授权,用户可以选择关闭。
浏览某个页面,例如:访问个人中心。
对于这种场景,我们可以在点击跳转某个页面的时候,进行拦截,弹窗处理。但这样的缺点是,跳转到目标页面的地方可能会很多,每个都拦截,难免会错漏。而且当目标页面作为「小程序落地页面」的时候,就避免不了。
这时候,我们可以通过重定向到授权页面来完成授权流程,完成之后,再回来。
那么我们定义一个枚举变量:
// 授权的展示形式export enum AuthDisplayMode { // 以弹窗形式 POPUP = 'button', // 以页面形式 PAGE = 'page', }复制代码
我们可以设计一个 mustAuth
方法,在点击某个按钮,或者页面加载的时候,进行授权控制。
伪代码示例:
class Session { // ... mustAuth({ mustAuthStep = AuthStepType.TWO, // 需要授权的LEVEL,默认需要获取用户资料 popupCompName = 'auth-popup', // 授权弹窗组件的 id mode = AuthDisplayMode.POPUP, // 默认以弹窗模式 } = {}): Promise<void> { // 如果当前的授权步骤已经达标,则返回成功 if (this.currentAuthStep() >= mustAuthStep) return Promise.resolve(); // 尝试获取当前页面的 <auth-popup></auth-popup> 组件实例 const pages = getCurrentPages(); const curPage = pages[pages.length - 1]; const popupComp = curPage.selectComponent(`#${popupCompName}`); // 组件不存在或者显示指定页面,跳转到授权页面 if (!popupComp || mode === AuthDisplayMode.PAGE) { const curRoute = curPage.route; // 跳转到授权页面,带上当前页面路由,授权完成之后,回到当前页面。 wx.redirectTo({ url: `authPage?backTo=${encodeURIComponent(curRoute)}` }); return Promise.resolve(); } // 设置授权 LEVEL,然后调用 <auth-popup> 的 nextStep 方法,进行进一步的授权。 popupComp.setMustAuthStep(mustAuthStep); popupComp.nextStep(); // 等待成功回调或者失败回调 return new Promise((resolve, reject) => { const authStatus = getApp().status.auth; authStatus.onceSuccess(resolve); authStatus.onceFail(reject); }); } // ...}复制代码</auth-popup></void>
那么我们就能在按钮点击,或者页面加载的时候进行授权拦截:
Page({ onLoad() { session.mustAuth().then(() => { // 开始初始化页面... }); } onClick(e) { session.mustAuth().then(() => { // 开始处理回调逻辑... }); } })复制代码
当然,如果项目使用了 TS 的话,或者支持 ES7 Decorator 特性的话,我们可以为 mustAuth
提供一个装饰器版本:
export function mustAuth(option = {}) { return function( _target, _propertyName, descriptor, ) { // 劫持目标方法 const method = descriptor.value; // 重写目标方法 descriptor.value = function(...args: any[]) { return session.mustAuth(option).then(() => { // 登录完成之后,重放原来方法 if (method) return method.apply(this, args); }); }; }; }复制代码
那么使用方式就简单一些了:
Page({ @mustAuth(); onLoad() { // 开始初始化页面... } @mustAuth(); onClick(e) { // 开始处理回调逻辑... } });复制代码
作为一套可复用的小程序登录方案,当然需要去定义好前后端的交互协议。
那么整套登录流程下来,需要的接口有这么几个:
静默登录 silentLogin
token
给前端token
前端会存起来,每个请求都会带上nickname
和phone
字段,前端用于计算当前用户的授权阶段。当然这个状态的记录可以放在后端,但是我们认为放在前端,会更加灵活。更新用户信息 updateUser
iv
, encryptedData
unionId
等nickname
等用户基本信息。session
中,用于计算授权阶段。更新用户手机号 updatePhone
iv
, encryptedData
session
中,用于计算授权阶段。解绑手机号 unbindPhone
登录 logout
入参:-
出参:-
说明:后端主动过期登录态,成功与否,走业务定义的前后端协议。
最后我们来梳理一下整体的「登录服务」的架构图:
由「登录服务」和「底层建设」组合提供的通用服务,业务层只需要去根据产品需求,定制授权的流程 <auth-flow></auth-flow>
,就能满足大部分场景了。
本篇文章通过一些常见的登录授权场景来展开来描述细节点。
整理了「登录」、「授权」的概念。
然后分别针对「登录」介绍了一些关键的技术实现:
session_key
Traitement tolérant aux pannes d'expirationQuant à "l'autorisation", il y aura la logique de conception de la partie UI, et elle doit également impliquer le fractionnement des composants :
Ensuite, les interfaces back-end sur lesquelles repose ce schéma d'autorisation de connexion sont triées et le protocole de référence le plus simple est donné.
Enfin, du point de vue "dans le but de précipiter un ensemble de solutions et de services universels de connexion à un mini-programme", j'ai réglé la superposition au niveau architectural.
Recommandations d'apprentissage gratuites associées : Développement du mini-programme WeChat
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!