微信小程序登陆的前端设计与实现

欢迎来个人博客阅读: 「微信小程序登陆的前端设计与实现」

一. 前言

对于登陆/注册的设计如此精雕细琢的目的,固然是想让这个做为应用的基础能力,有足够的健壮性,避免出现全站性的阻塞。javascript

同时要充分考虑如何解耦和封装,在开展新的小程序的时候,能更快的去复用能力,避免重复采坑。html

登陆注册这模块,就像个冰山,咱们觉得它就是「输入帐号密码,就完成登陆了」,但实际下面还有各类须要考虑的问题。前端

在此,跟在座的各位分享一下,最近作完一个小程序登陆/注册模块以后,沉淀下来的一些设计经验和想法。java

二. 业务场景

在用户浏览小程序的过程当中,由业务须要,每每须要获取用户的一些基本信息,常见的有:git

  1. 微信昵称
  2. 微信手机号

而不一样的产品,对于用户的信息要求不尽相同,也会有不同的受权流程。github

第一种,常见于电商系统中,用户购买商品的时候,为了识别用户多平台的帐号,每每用手机号去作一个联系,这时候须要用户去受权手机号。小程序

受权手机号

第二种,为了让用户信息获得基本的初始化,每每须要更进一步获取用户信息:如微信昵称,unionId 等,就须要询问用户受权。后端

受权用户信息

第三种,囊括第一种,第二种。微信小程序

完整受权流程

三. 概念

秉着沉淀一套通用的小程序登陆方案和服务为目标,咱们去分析一下业务,得出变量。api

在作技术设计以前,讲点必要的废话,对一些概念进行基本调频。

2.1 关于「登陆」

登陆在英文中是 「login」,对应的还有 「logout」。而登陆以前,你须要拥有一个帐号,就要 「register」(or sign up)。

话说一开始的产品是没有登陆/注册功能的,用的人多了就慢慢有了。出于产品自己的需求,须要对「用户」进行身份识别。

在现实社会中,咱们每一个人都有一个身份ID:身份证。当我到了16岁的时候,第一次去公安局领身份证的时候,就完成了一次「注册」行为。而后我去网吧上网,身份证刷一下,完成了一次「登陆」行为。

那么对于虚拟世界的互联网来讲,这个身份证实就是「帐号+密码」。

常见的登陆/注册方式有:

  1. 帐号密码注册

    在互联网的早期,我的邮箱和手机覆盖度小。因此,就须要用户本身想一个帐号名,咱们注册个QQ号,就是这种形式。

    from 汽车之家

  2. 邮箱地址注册

    千禧年以后,PC互联网时代快速普及,咱们都建立了属于本身的我的邮箱。加上QQ也自带邮箱帐号。因为邮箱具备我的私密性,且可以进行信息的沟通,所以,大部分网站开始采用邮箱帐号做为用户名来进行注册,而且会在注册的过程当中要求登陆到相应邮箱内查收激活邮件,验证咱们对该注册邮箱的全部权。

    from 支付宝

  3. 手机号码注册

    在互联网普及以后,智能手机与移动互联网发展迅猛。手机也成为每一个人必不可少的移动设备,同时移动互联网也已经深深融入每一个人的现代生活当中。因此,相较于邮箱,目前手机号码与我的的联系更加紧密,并且愈来愈多的移动应用出现,采用手机号码做为用户名的注册方式也获得了普遍的使用。

    from 知乎

到了 2020 年,微信用户规模达 12 亿。那么,微信帐号,起码在中国,已成为新一代互联网世界的「身份标识」。

而对微信小程序而言,自然就能知道当前用户的微信帐号ID。微信容许小程序应用,能在用户无感知的状况下,悄无声息的「登陆」到咱们的小程序应用中去,这个就是咱们常常称之为的「静默登陆」。

其实微信小程序的登陆,跟传统 Web 应用的「单点登陆」本质是同样的概念。

  1. 单点登陆:在 A 站登陆了,C 站和 B 站能实现快速的「静默登陆」。
  2. 微信小程序登陆:在微信中,登陆了微信帐号,那么在整个小程序生态中,均可以实现「静默登陆」。

因为 Http 原本是无状态的,业界基本对于登陆态的通常作法:

  1. cookie-session:经常使用于浏览器应用中
  2. access token:经常使用于移动端等非浏览器应用

在微信小程序来讲,对于「JS逻辑层」并非一个浏览器环境,天然没有 Cookie,那么一般会使用 access token 的方式。

2.2 关于「受权」

对于须要更进一步获取用的用户昵称、用户手机号等信息的产品来讲。微信出于用户隐私的考虑,须要用户主动赞成受权。小程序应用才能获取到这部分信息,这就有了目前流行的小程序「受权用户信息」、「受权手机号」的交互了。

出于不一样的用户信息敏感度不一样的考虑,微信小程序对于不一样的用户信息提供「受权」的方式不尽相同:

  1. 调用具体 API 方式,弹窗受权。

    1. 例如调用 wx.getLocation() 的时候,若是用户未受权,则会弹出地址受权界面。
    2. 若是拒绝了,就不会再次弹窗,wx.getLocation()直接返回失败。
  2. <button open-type="xxx" /> 方式。

    1. 仅支持:用户敏感信息,用户手机号,须要配合后端进行对称加解密,方能拿到数据。
    2. 用户已拒绝,再次点击按钮,仍然会弹窗。
  3. 经过 wx.authorize(),提早询问受权,以后须要获取相关信息的时候不用再次弹出受权。

四. 详细设计

梳理清楚了概念以后,咱们模块的划分上,能够拆分为两大块:

  1. 登陆:负责与服务端建立起一个会话,这个会话实现静默登陆以及相关的容错处理等,模块命名为:Session
  2. 受权:负责与用户交互,获取与更新信息,以及权限的控制处理等,模块命名为:Auth

3.1 登陆的实现

3.1.1 静默登陆

微信登陆

微信官方提供的登陆方案,总结为三步:

  1. 前端经过 wx.login() 获取一次性加密凭证 code,交给后端。
  2. 后端把这个 code 传输给微信服务器端,换取用户惟一标识 openId 和受权凭证 session_key。(用于后续服务器端和微信服务器的特殊 API 调用,具体看:微信官方文档-服务端获取开放数据)。
  3. 后端把从微信服务器获取到的用户凭证与自行生成的登陆态凭证(token),传输给前端。前端保存起来,下次请求的时候带给后端,就能识别哪一个用户。

若是只是实现这个流程的话,挺简单的。

但要实现一个健壮的登陆过程,还须要注意更多的边界状况:

  1. 收拢 wx.login() 的调用

    因为 wx.login() 会产生不可预测的反作用,例如会可能致使session_key失效,从而致使后续的受权解密场景中的失败。咱们这里能够提供一个像 session.login() 的方法,掌握 wx.login() 控制权,对其作一系列的封装和容错处理。

  2. 调用的时机

    一般咱们会在应用启动的时候( app.onLaunch() ),去发起静默登陆。但这里会由小程序生命周期设计问题而致使的一个异步问题:加载页面的时候,去调用一个须要登陆态的后端 API 的时候,前面异步的静态登陆过程有可能尚未完成,从而致使请求失败。

    固然也能够在第一个须要登陆态的接口调用的时候以异步阻塞的方式发起登陆调用,这个须要结合良好设计的接口层。

    以上讲到的两种场景的详细设计思路下文会讲到。

  3. 并发调用的问题

    在业务场景中,不免会出现多处代码须要触发登陆,若是遇到极端状况,这多处代码同时间发起调用。那就会形成短期屡次发起登陆过程,尽管以前的请求尚未完成。针对这种状况,咱们能够以第一个调用为阻塞,后续调用等待结果,就像精子和卵子结合的过程。

  4. 未过时调用的问题

    若是咱们的登陆态未过时,彻底能够正常使用的,默认状况就不需再去发起登陆过程了。这时候咱们能够默认状况下先去检查登陆态是否可用,不能用,咱们再发起请求。而后还能够提供一个相似 session.login({ force: true })的参数去强行发起登陆。

3.1.2 静默登陆异步状态的处理

1. 应用启动的时候调用

由于大部分状况都须要依赖登陆态,咱们会很天然而然的想到把这个调用的时机放到应用启动的时候( app.onLaunch() )来调用。

可是因为原生的小程序启动流程中, AppPageComponent 的生命周期钩子函数,都不支持异步阻塞。

那么咱们很容易会遇到 app.onLaunch 发起的「登陆过程」在 page.onLoad 的时候尚未完成,咱们就没法正确去作一些依赖登陆态的操做。

针对这种状况,咱们设计了一个状态机的工具:status

状态机

基于状态机,咱们就能够编写这样的代码:

import { Status } from '@beautywe/plugin-status';

// on app.js
App({
    status: {
       login: new Status('login');
    },

    onLaunch() {
        session
            // 发起静默登陆调用
            .login()

            // 把状态机设置为 success
            .then(() => this.status.login.success())
      
            // 把状态机设置为 fail
            .catch(() => this.status.login.fail());
    },
});


// on page.js
Page({
    onLoad() {
      const loginStatus = getApp().status.login;
      
      // must 里面会进行状态的判断,例如登陆中就等待,登陆成功就直接返回,登陆失败抛出等。
      loginStatus().status.login.must(() => {
        // 进行一些须要登陆态的操做...
      });
    },
});

2. 在「第一个须要登陆态接口」被调用的时候去发起登陆

更进一步,咱们会发现,须要登陆态的更深层次的节点是在发起的「须要登陆态的后端 API 」的时候。

那么咱们能够在调用「须要登陆态的后端 API」的时候再去发起「静默登陆」,对于并发的场景,让其余请求等待一下就行了。

fly.js 做为 wx.request() 封装的「网络请求层」,作一个简单的例子:

// 发起请求,并代表该请求是须要登陆态的
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;
});

3.1.3 自定义登陆态过时的容错处理

当自定义登陆态过时的时候,后端须要返回特定的状态码,例如:AUTH_EXPIREDAUTH_INVALID 等。

前端能够在「网络请求层」去监听全部请求的这个状态码,而后发起刷新登陆态,再去重放失败的请求:

// 添加响应拦截器
fly.interceptors.response.use(
    (response) => {
      const code = res.data;
        
      // 登陆态过时或失效
      if ( ['AUTH_EXPIRED', 'AUTH_INVALID'].includes(code) ) {
      
        // 刷新登陆态
        await session.refreshLogin();
        
        // 而后从新发起请求
        return fly.request(request);
      }
    }
)

那么若是并发的发起多个请求,都返回了登陆态失效的状态码,上述代码就会被执行屡次。

咱们须要对 session.refreshLogin() 作一些特殊的容错处理:

  1. 请求锁:同一时间,只容许一个正在过程当中的网络请求。
  2. 等待队列:请求被锁定以后,调用该方法的全部调用,都推入一个队列中,等待网络请求完成以后共用返回结果。
  3. 熔断机制:若是短期内屡次调用,则中止响应一段时间,相似于 TCP 慢启动。

示例代码:

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;
          });
      });

  // ...
}

3.1.4 微信 session_key 过时的容错处理

咱们从上面的「静默登陆」以后,微信服务器端会下发一个 session_key 给后端,而这个会在须要获取微信开放数据的时候会用到。

微信开放数据

session_key 是有时效性的,如下摘自微信官方描述:

会话密钥 session_key 有效性

开发者若是遇到由于 session_key 不正确而校验签名失败或解密失败,请关注下面几个与 session_key 有关的注意事项。

  1. wx.login 调用时,用户的 session_key 可能会被更新而导致旧 session_key 失效(刷新机制存在最短周期,若是同一个用户短期内屡次调用 wx.login,并不是每次调用都致使 session_key 刷新)。开发者应该在明确须要从新登陆时才调用 wx.login,及时经过 auth.code2Session 接口更新服务器存储的 session_key。
  2. 微信不会把 session_key 的有效期告知开发者。咱们会根据用户使用小程序的行为对 session_key 进行续期。用户越频繁使用小程序,session_key 有效期越长。
  3. 开发者在 session_key 失效时,能够经过从新执行登陆流程获取有效的 session_key。使用接口 wx.checkSession能够校验 session_key 是否有效,从而避免小程序反复执行登陆流程。
  4. 当开发者在实现自定义登陆态时,能够考虑以 session_key 有效期做为自身登陆态有效期,也能够实现自定义的时效性策略。

翻译成简单的两句话:

  1. session_key 时效性由微信控制,开发者不可预测。
  2. wx.login 可能会致使 session_key 过时,能够在使用接口以前用 wx.checkSession 检查一下。

而对于第二点,咱们经过实验发现,偶发性的在 session_key 已过时的状况下,wx.checkSession 会几率性返回 true

社区也有相关的反馈未获得解决:

因此结论是:wx.checkSession可靠性是不达 100% 的。

基于以上,咱们须要对 session_key 的过时作一些容错处理:

  1. 发起须要使用 session_key 的请求前,作一次 wx.checkSession 操做,若是失败了刷新登陆态。
  2. 后端使用 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' });
      }
    }
)

3.2 受权的实现

3.2.1 组件拆分与设计

在用户信息和手机号获取的方式上,微信是以 <button open-type='xxx' /> 的方式,让用户主动点击受权的。

那么为了让代码更解耦,咱们设计这样三个组件:

  1. <user-contaienr getUserInfo="onUserInfoAuth">: 包装点击交互,经过 <slot> 支持点击区域的自定义UI。
  2. <phone-container getPhonenNmber="onPhoneAuth"> : 与 <user-container> 同理。
  3. <auth-flow>: 根据业务须要,组合 <user-container><phone-container> 组合来定义不一样的受权流程。

以开头的业务场景的流程为例,它有这样的要求:

  1. 有多个步骤。
  2. 若是中途断掉了,能够从中间接上。
  3. 有些场景中,只要求达到「用户信息受权」,而不须要完成「用户手机号」。

完整受权流程

那么受权的阶段能够分三层:

// 用户登陆的阶段
export enum AuthStep {
  // 阶段一:只有登陆态,没有用户信息,没有手机号
  ONE = 1,

  // 阶段二:有用户信息,没有手机号
  TWO = 2,

  // 阶段三:有用户信息,有手机号
  THREE = 3,
}

AuthStep 的推动过程是不可逆的,咱们能够定义一个 nextStep 函数来封装 AuthStep 更新的逻辑。外部使用的话,只要无脑调用 nextStep 方法,等待回调结果就行。

示例伪代码:

// auth-flow component

Component({
  // ...
  
  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) {
      // 更新全局的受权状态机,广播消息给订阅者。
      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> 中就能够根据 currAuthStepmustAuthStep 来去作不一样的 UI 展现。须要注意的是使用 <user-container><phone-container> 的时候链接上 nextStep(e) 函数。

示例伪代码:

<view class="auth-flow">

  <!-- 已完成受权 -->
  <block wx:if="{{currAuthStep === mustAuthStep || currAuthStep === AuthStep.THREE}}">
    <view>已完成受权</view>
  </block>

  <!-- 未完成受权,第一步:受权用户信息 -->
  <block wx:elif="{{currAuthStep === AuthStep.ONE}}">
    <user-container bind:getuserinfo="nextStep">
      <view>受权用户信息</view>
    </user-container>
  </block>

  <!-- 未完成受权,第二步:受权手机号 -->
  <block wx:elif="{{currAuthStep === AuthStep.TWO}}">
    <phone-container bind:getphonenumber="nextStep">
      <view>受权手机号</view>
    </phone-container>
  </block>
  
</view>

3.2.2 权限拦截的处理

到这里,咱们制做好了用来承载受权流程的组件 <auth-flow> ,那么接下来就是决定要使用它的时机了。

咱们梳理须要受权的场景:

  1. 点击某个按钮,例如:购买某个商品。

    对于这种场景,常见的是经过弹窗完成受权,用户能够选择关闭。

    受权模型-弹窗

  2. 浏览某个页面,例如:访问我的中心。

    对于这种场景,咱们能够在点击跳转某个页面的时候,进行拦截,弹窗处理。但这样的缺点是,跳转到目标页面的地方可能会不少,每一个都拦截,不免会错漏。并且当目标页面做为「小程序落地页面」的时候,就避免不了。

    这时候,咱们能够经过重定向到受权页面来完成受权流程,完成以后,再回来。

    受权模型-页面

那么咱们定义一个枚举变量:

// 受权的展现形式
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 id="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);
    });
  }
  
  // ...
}

那么咱们就能在按钮点击,或者页面加载的时候进行受权拦截:

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) {
    // 开始处理回调逻辑...
  }
});

3.3. 先后端交互协议整理

做为一套可复用的小程序登陆方案,固然须要去定义好先后端的交互协议。

那么整套登陆流程下来,须要的接口有这么几个:

登陆注册先后端接口协议

  1. 静默登陆 silentLogin

    1. 入参:

      1. code: 产自 wx.login()
    2. 出参:

      1. token: 自定义登陆态凭证
      2. userInfo: 用户信息
    3. 说明:

      1. 后端利用 code 跟微信客户端换取用户标识,而后注册并登陆用户,返回自定义登陆态 token 给前端
      2. token 前端会存起来,每一个请求都会带上
      3. userInfo 须要包含nicknamephone字段,前端用于计算当前用户的受权阶段。固然这个状态的记录能够放在后端,可是咱们认为放在前端,会更加灵活。
  2. 更新用户信息 updateUser

    1. 入参:

      1. nickname: 用户昵称
      2. encrypt: 微信开放数据相关的 iv, encryptedData
      3. 以及其余如性别地址等非必要字段
    2. 出参:

      1. userInfo:更新后的最新用户信息
    3. 说明:

      1. 后端解密微信开放数据,获取隐蔽数据,如:unionId
      2. 后端支持更新包括 nickname等用户基本信息。
      3. 前端会把 userInfo 信息更新到 session 中,用于计算受权阶段。
  3. 更新用户手机号 updatePhone

    1. 入参:

      1. encrypt:微信开放数据相关的 iv, encryptedData
    2. 出参:

      1. userInfo:更新后的最新用户信息
    3. 说明:

      1. 后端解密开放式局,获取手机号,并更新到用户信息中。
      2. 前端会把 userInfo 信息更新到 session 中,用于计算受权阶段。
  4. 解绑手机号 unbindPhone

    1. 入参:-
    2. 出参:-
    3. 说明:后端解绑用户手机号,成功与否,走业务定义的先后端协议。
  5. 登陆 logout

    1. 入参:-
    2. 出参:-
    3. 说明:后端主动过时登陆态,成功与否,走业务定义的先后端协议。

五. 架构图

最后咱们来梳理一下总体的「登陆服务」的架构图:

微信小程序登陆服务架构图

由「登陆服务」和「底层建设」组合提供的通用服务,业务层只须要去根据产品需求,定制受权的流程 <auth-flow> ,就能知足大部分场景了。

六. 总结

本篇文章经过一些常见的登陆受权场景来展开来描述细节点。

整理了「登陆」、「受权」的概念。

而后分别针对「登陆」介绍了一些关键的技术实现:

  1. 静默登陆
  2. 静默登陆异步状态的处理
  3. 自定义登陆态过时的容错处理
  4. 微信 session_key 过时的容错处理

而对于「受权」,会有设计UI部分的逻辑,还须要涉及到组件的拆分:

  1. 组件拆分与设计
  2. 权限拦截的处理

而后,梳理了这套登陆受权方案所依赖的后端接口,和给出最简单的参考协议。

最后,站在「秉着沉淀一套通用的小程序登陆方案和服务为目标」的角度,梳理了一下架构层面上的分层。

  1. 业务定制层
  2. 登陆服务层
  3. 底层建设

七. 参考

  1. fly.js 官网
  2. 微信官方文档-受权
  3. 微信官方文档-服务端获取开放数据
  4. 微信官方社区

    1. 小程序解密手机号,隔一小段时间后,checksession:ok,可是解密失败
    2. wx.checkSession有效,可是解密数据失败
    3. checkSession判断session_key未失效,可是解密手机号失败
相关文章
相关标签/搜索