7.1. 用户管理

7.1.1. 功能概述

本文档主要介绍用户管理功能,您可以通过本文示例体验用户注册、登录、退出登录功能。

7.1.2. 体验功能

https://main.qcloudimg.com/raw/c3bffdddfa54ecd95b5a569b10f71840.png

7.1.3. 体验 DEMO

本章的案例代码,请参考 tcb-demo-basic

7.1.4. DEMO 接入流程

代码下载完成后,请按照以下步骤操作:

  1. 在云开发中创建好至少一个环境。

  2. 在 app.js 文件中,如果使用默认环境,则没有改动。 如果需要使用非默认环境,则需要在 wx.cloud.init 中传入环境 ID。 如果使用非默认环境,在云函数的 cloud.init 处,也需要补充环境 ID。

如下图所示:

https://main.qcloudimg.com/raw/69d8d7590284cc80776021f3f8883948.png

示例代码如下:

wx.cloud.init({
  env: 'xxxx', // 环境 id
  traceUser: true
});
  1. 在云函数根目录 cloud/functions 中找到函数 user-login-register 和 user-session, 在两者的 config 目录中新建 index.js,填入小程序的密钥 AppSecret,用 npm 安装两个云函数的依赖,并上传两个云函数。

https://main.qcloudimg.com/raw/532124c3661e43c86cddca24a85ad508.png
  1. 在云开发的数据库中,新建 collection,名为 users。

7.1.5. 源码介绍

7.1.5.1. 准备工作

用户管理,包括用户的信息(昵称、性别、头像等的获取)、注册、登录、鉴权等。 本节主要介绍云开发如何操作用户管理。

开发前,建议先阅读以下相关的文档:微信登录能力优化 和 获取用户信息。

参考常用的小程序,例如知乎大学、百果园、摩拜等。

https://main.qcloudimg.com/raw/085b0eee6a5533f80c4708eb29ad7c98.png

基本的流程:

  • 用户授权小程序可获取用户的开放数据。

  • 选择登录方式(微信绑定的手机/用户的其它手机)。

  • 如果选用了微信绑定的手机,直接信任,注册/登录成功,而如果选用其它手机,则还需要通过发送短信进行手机验证。

7.1.5.2. 用户登录、注册与信息

用户的登录、注册、获取信息过程,涉及到以下接口。 详情请参考 用户信息 文档。

  • wx.getSetting,看看用户有没有授权小程序,可以获取昵称、头像、性别等的用户信息。

  • wx.getUserInfo (旧版) / button (新版),授权后,可通过此接口/组件获取用户信息。

  • wx.checkSession,获取 session_key 后,要检查 session_key 是否过期。

  • 如果 session_key 过期,则通过 wx.login 获取 code 后,在云函数中调用 code2Session 更新 session_key。

  • 通过 button 组件,获取手机号码加密数据,并在云函数中通过之前取得的 session_key 进行解密获取真实手机号码。

流程图如下所示:

https://main.qcloudimg.com/raw/e2cd0d8be1e4bb3809ce1eedf8d4f820.png

7.1.5.3. 用户授权

对于小程序来说,必须进行用户授权,才能在后续中获取用户的开放数据。 因此,我们在 onLoad 生命周期里,做了以下处理:

  • 旧版: 做授权的检测。

  • 新版: 在模板文件中,则设置授权按钮。

授权后,请将用户数据存入临时的对象中,示例代码如下:

onLoad: async function (options) {
  this.db = wx.cloud.database();
  this.checkAuthSetting();
  this.checkUser();
},

// 检测权限,在旧版小程序若未授权会自己弹起授权
checkAuthSetting() {
  wx.getSetting({
    success: (res) => {
      if (res.authSetting['scope.userInfo']) {
        wx.getUserInfo({
          success: async (res) => {
            if (res.userInfo) {
              const userInfo = res.userInfo
              // 将用户数据放在临时对象中,用于后续写入数据库
              this.setUserTemp(userInfo)
            }

            const userInfo = this.data.userInfo || {}
            userInfo.isLoaded = true
            this.setData({
              userInfo,
              isAuthorized: true
            })
          }
        })
      } else {
        this.setData({
          userInfo: {
            isLoaded: true,
          }
        })
      }
    }
  })
},

// 设置临时数据,待 “真正登录” 时将用户数据写入 collection "users" 中
setUserTemp(userInfo = null, isAuthorized = true, cb = () => {}) {
  this.setData({
    userTemp: userInfo,
    isAuthorized,
  }, cb)
},

// 手动获取用户数据
async bindGetUserInfoNew(e) {
  const userInfo = e.detail.userInfo
  // 将用户数据放在临时对象中,用于后续写入数据库
  this.setUserTemp(userInfo)
},
<button
  wx:if="{{userInfo.isLoaded && !isAuthorized && !userInfo.nickName}}"
  class="weui-btn"
  type="primary"
  open-type="getUserInfo"
  bindgetuserinfo="bindGetUserInfoNew"
>
  授权微信后登录
</button>

7.1.5.4. 数据解密

由于某些数据的安全性问题,需要在后台服务进行解密,譬如手机号码,我们可以借助云开发的云函数去完成。

通过 checkUser 方法,检测 session_key 是否已经过期;如果过期,则重新设置,并将用户写入数据库中。 此逻辑通过 updateSession 方法和云函数 user-session 共同完成。

示例代码如下:

// 检测小程序的 session 是否有效
async checkUser() {
  const Users = this.db.collection('users')
  const users = await Users.get()
  console.log(users)

  wx.checkSession({
    success: () => {
      // session_key 未过期,并且在本生命周期一直有效
      // 数据里有用户,则直接获取
      if (users.data.length && this.checkSession(users.data[0].expireTime || 0)) {
        this.setUserInfo(users.data[0])
      } else {
        this.setUserInfo()
        // 强制更新并新增了用户
        this.updateSession()
      }
    },
    fail: () => {
      // session_key 已经失效,需要重新执行登录流程
      this.updateSession()
    }
  })
},

// 更新 session_key
updateSession() {
  wx.login({
    success: async (res) => {
      console.log(res)
      try {
        await wx.cloud.callFunction({
          name: 'user-session',
          data: {
            code: res.code
          }
        })
      } catch (e) {
        console.log(e)
      }
    }
  })
},

以下是云函数 user-session 的源码,它的主要作用是通过 wXMINIUser.codeToSession 方法获取最新 session_key 。

  • 如果有数据,则仅更新 session_key,

  • 如果没数据则添加该用户并插入 sesison_key。 此 session_key 与用户的数据关联,为后续进行数据解密打下了基础。

// 云函数入口函数
exports.main = async (event) => {
  console.log(event)
  const db = cloud.database()

  const {
    OPENID,
    APPID
  } = cloud.getWXContext()

  const wXMINIUser = new WXMINIUser({
    appId: APPID,
    secret
  })

  const code = event.code // 从小程序端的 wx.login 接口传过来的 code 值
  const info = await wXMINIUser.codeToSession(code)

  try {
    // 查询有没用户数据
    const user = await db.collection('users').where({
      _openid: OPENID
    }).get()

    // 如果有数据,则只是更新 `session_key`,如果没数据则添加该用户并插入 `sesison_key`
    if (user.data.length) {
      await db.collection('users').where({
        _openid: OPENID
      }).update({
        data: {
          session_key: info.session_key
        }
      })
    } else {
      await db.collection('users').add({
        data: {
          session_key: info.session_key,
          _openid: OPENID
        }
      })
    }
  } catch (e) {
    return {
      message: e.message,
      code: 1,
    }
  }

  return {
    message: 'login success',
    code: 0
  }
}

当我们在数据中存入了用户的一条空数据以及它相关的 session_key 后,我们可以引导用户通过小程序获取微信绑定的手机号,实现快速登录。 在模板文件中,我们添加了一个 button 组件,并将 open-type 设置为 getPhoneNumber。

<button
  wx:if="{{userInfo.isLoaded && isAuthorized && !userInfo.phoneNumber}}"
  class="weui-btn"
  type="primary"
  open-type="getPhoneNumber"
  bindgetphonenumber="bindGetPhoneNumber"
>
  微信快速登录
</button>

单击【微信快速登录】后,请调用 bindGetPhoneNumber,将存放于临时对象的用户开放数据, 以及加密的微信手机数据,发送到 user-login-register 进行解密,并存入用户的数据中。

// 获取用户手机号码
async bindGetPhoneNumber(e) {
  // console.log(e.detail);
  wx.showLoading({
    title: '正在获取',
  })

  try {
    const data = this.data.userTemp
    const result = await wx.cloud.callFunction({
      name: 'user-login-register',
      data: {
        encryptedData: e.detail.encryptedData,
        iv: e.detail.iv,
        user: {
          nickName: data.nickName,
          avatarUrl: data.avatarUrl,
          gender: data.gender
        }
      }
    })
    console.log(result)

    if (!result.result.code && result.result.data) {
      this.setUserInfo(result.result.data)
    }

    wx.hideLoading()
  } catch (err) {
    wx.hideLoading()
    wx.showToast({
      title: '获取手机号码失败,请重试',
      icon: 'none'
    })
  }
},

详细解密数据的原理,请参考 开放数据校验与解密

const WXBizDataCrypt = require('./WXBizDataCrypt');
const {
  appId,
  secret
} = require('./config');
const cloud = require('wx-server-sdk');

const duration = 24 * 3600 * 1000; // 开发侧控制登录态有效时间

cloud.init();

// 云函数入口函数
const cloud = require('wx-server-sdk')
const WXBizDataCrypt = require('./WXBizDataCrypt')

const duration = 24 * 3600 * 1000 // 开发侧控制登录态有效时间

cloud.init()

// 云函数入口函数
exports.main = async (event) => {
  const {
    OPENID,
    APPID
  } = cloud.getWXContext()

  const db = cloud.database()
  const users = await db.collection('users').where({
    _openid: OPENID
  }).get()

  if (!users.data.length) {
    return {
      message: 'user not found',
      code: 1
    }
  }

  // 进行数据解密
  const user = users.data[0]
  const wxBizDataCrypt = new WXBizDataCrypt(APPID, user.session_key)
  const data = wxBizDataCrypt.decryptData(event.encryptedData, event.iv)

  const expireTime = Date.now() + duration

  try {
    // 将用户数据和手机号码数据更新到该用户数据中
    const result = await db.collection('users').where({
      _openid: OPENID
    }).update({
      data: {
        ...event.user,
        phoneNumber: data.phoneNumber,
        expireTime
      }
    })

    if (!result.stats.updated) {
      return {
        message: 'update failure',
        code: 1
      }
    }
  } catch (e) {
    return {
      message: e.message,
      code: 1
    }
  }


  return {
    message: 'success',
    code: 0,
    data: {
      ...event.user,
      ...data
    },
  }
}

7.1.5.5. 退出登录

  • 如果不需要用户退出登录,单纯依赖 wx.checkSession 就可以作为用户登录态失效的办法。

  • 如果需要允许用户主动退出,请参考以下配置方法。

您可以在用户数据里添加一个 expireTime 字段,用于记录用户登录态失效的时间, 在云函数 user-login-register 里就有 expireTime 的相关配置和写入逻辑。 示例代码如下:

// 节选自 `user-login-register`
const duration = 24 * 3600 * 1000; // 开发侧控制登录态有效时间,此处表时24小时,即1天

// ...... 此处省略其它代码

// 将 expireTime 写入用户数据里
const result = await db.collection('users').where({
  _openid: OPENID
}).update({
  data: {
    ...event.user,
    phoneNumber: data.phoneNumber,
    expireTime
  }
})

在 checkUser 方法中,您也可以调用 checkSession 去检测用户数据中的 expireTime 是否过期, 如果过期,则不会再展示用户数据,并更新一下 session_key。

示例代码如下:

// 检查用户是否登录态还没过期
checkSession(expireTime = 0) {
  if (Date.now() > expireTime) {
    return false;
  }

  return true;
},

以下此方法,则是用户主动单击退出登录按钮后,触发的方法,会将用户的 expireTime 设零过期。

示例代码如下:

// 退出登录
async bindLogout() {
  const userInfo = this.data.userInfo

  await this.db.collection('users').doc(userInfo._id).update({
    data: {
      expireTime: 0
    }
  })

  this.setUserInfo()
}

至此,您已基本完成一个简单有效的用户注册、登录页面。