Taro 小程序开发大型实战(九):使用 Authing 打造企业级用户系统

1,998 阅读15分钟

如果您觉得我们写得还不错,记得 点赞 + 关注 + 评论 三连,鼓励我们写出更好的教程💪

欢迎继续阅读《Taro 小程序开发大型实战》系列,前情回顾:

之前我们的小程序具有了一个简单博客必备的一些功能:

  • 权限管理:发帖之前需要登录
  • 登录:普通登录和微信登录等
  • 发帖:帖子会自动带上用户信息
  • 获取所有帖子和单个帖子

乍一看这个博客有点小完整了,但是一路跟下来的同学应该知道,我们之前的登录都是通过传入用户的 nickNamephoto 来登录的,但是我们一般在生活中看到的一些比较正规的网站或者小程序,它们的登录一般都有类似手机+验证码登录,并且在一个标准的博客里面,可能还会涉及到诸如用户权限管理,用户登录状态查询等,刚刚我提到的种种关于用户的场景一般会被抽象为一个应用里的一个核心模块 -- 用户系统,即所有和用户注册/登录、信息更新、权限管理、鉴权等相关的内容。

为了让我们的博客看起来更加专业,我们打算给它也加上整上一个专业的用户系统,有了最为核心的用户系统在,我们博客之后的扩展都可以游刃有余,但是据统计,一个应用要想打造一个比较专业的用户系统,至少需要花费几个月时间,还需要花大量的精力去维护打造出来的用户系统,所在在做了一番调研之后,我们将目标放在了一个叫做 Authing 的通用云身份平台,它提供的服务就是帮应用快速集成一个高效、安全的用户系统,而我们这篇教程将会讲解如何借助 Authing 来给我们的之前的小程序博客武装一个专业的用户系统。

首先我们先来看一看完成的效果:

2020-04-30 15-25-51.2020-04-30 15_27_30.gif

准备新版登录逻辑

如果你希望直接从这篇开始,那么可以 Clone 我们为你准备的代码,然后跟着教程补充剩下的部分:

git clone -b authing-start https://github.com/tuture-dev/ultra-club.git
# 或者下载 Gitee 上的仓库
git clone -b authing-start https://gitee.com/tuture/ultra-club.git

改进普通登录

首先我们来将之前普通登录的专业性提升一个档次,之前我们是让用户输入昵称和上传头像然后进行登录,现在我们打算切换到手机号+验证码的形式,立马现代化。

打开 src/components/LoginForm/index.jsx 文件,对其中的内容作出对应的修改如下:

import Taro, { useState, useRef, useEffect } from '@tarojs/taro'
import { View, Form } from '@tarojs/components'
import { AtButton, AtImagePicker } from 'taro-ui'
import { useDispatch } from '@tarojs/redux'

import { SET_IS_OPENED, SET_LOGIN_INFO } from '../../constants'
import CountDownButton from '../CountDownButton'
import './index.scss'

export default function LoginForm(props) {
  // Login Form 登录数据
  const [phone, setPhone] = useState('')
  const [phoneCode, setPhoneCode] = useState('')
  const countDownButtonRef = useRef(null)

  const dispatch = useDispatch()

  async function countDownButtonPressed() {
    if (!phone) {
      Taro.atMessage({
        type: 'error',
        message: '您还没有填写手机!',
      })

      return
    }

    countDownButtonRef.current.startCountDown()

    // 处理发送验证码事件
  }

  async function handleSubmit(e) {
    e.preventDefault()

    // 鉴权数据
    if (!phone || !phoneCode) {
      Taro.atMessage({
        type: 'error',
        message: '您还有内容没有填写!',
      })

      return
    }

    // 处理登录和注册
  }

  return (
    <View className="post-form">
      <Form onSubmit={handleSubmit}>
        <View className="login-box">
          <Input
            className="input-phone input-item"
            type="text"
            placeholder="输入手机号"
            value={phone}
            onInput={e => setPhone(e.target.value)}
          />
          <View className="verify-code-box">
            <Input
              className="input-nickName input-item"
              type="text"
              placeholder="四位验证码"
              value={phoneCode}
              onInput={e => setPhoneCode(e.target.value)}
            />
            <CountDownButton
              onClick={countDownButtonPressed}
              ref={countDownButtonRef}
            />
          </View>
          <AtButton formType="submit" type="primary">
            登录
          </AtButton>
          <View className="at-article__info">
            通过手机和验证码来登录,如果没有账号,我们将自动创建
          </View>
        </View>
      </Form>
    </View>
  )
}

可以看到,上面的代码主要有如下几处更改:

  • 删除了处理 avatarnickNameuseState 逻辑
  • 删除了之前用于处理上传头像的 onImageClickonChange 函数,以及 AtImagePicker 组件
  • 改进和增加了两个输入框,一个用于输入手机号,一个用于输入验证码,同是增加了 phonephoneCodeuseState 逻辑
  • 改进 handleSubmit ,删除了原处理 nickNamefiles 的逻辑,以及删除了之前发起登录的 dispatch 逻辑
  • 接着我们增加了一个用于倒计时的 CountDownButton 组件,以及获取 refcountDownButtonRef 和处理按钮点击事件的 countDownButtonPressed 函数,在函数里面我们会做数据验证,如果用户填写了手机号,才允许执行倒计时逻辑,在接下来我们将在这个函数里面处理手机验证码发送逻辑。
  • 最后我们添加了提示用户使用手机和验证码登录的文案。

提示 这里的 CountDownButton 是 Taro 官方物料市场提供的物料,可以访问 这个地址,下载物料,然后将 CountDownButton 的文件夹放到 src/compontents 文件夹下面。我们还需要对这个组件的样式做一点修改,以适应我们现在的 UI 风格,我们将马上来讲解如何修改,读者先可以下载这个物料,然后放置到刚刚提到的项目目录下。

样式改进

上面我们改进了组件,为了让我们的新版登录样子看起来更加专业、统一,我们加点样式,打开 src/components/LoginForm/index.scss 文件,对其中的内容作出对应的修改如下:

.post-form {
  margin: 0 30px;
  padding: 30px;
}

.input-item {
  border: 1px solid #eee;
  padding: 10px;
  font-size: medium;
  margin-top: 40px;
  margin-bottom: 40px;
}

.input-phone {
  width: 100%;
}

.input-nickName {
  width: calc(100% - 200px);
}

.verify-code-box {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
}

.avatar-selector {
  width: 200px;
  margin: 0 auto;
}

使用 Taro 物料

我们上面用到的 CountDownButton 组件,就是 Taro 物料市场的一个物料, 简单的说物料就是一个能某方面功能完善的包或组件,帮助开发者快速完成某个逻辑而不需要重复造轮子,正如 Taro 物料市场官方的标语:

让每一个让每一个轮子产生价值

我们通过之前的步骤,应该已经下载好了物料,并放在了 src/components 文件夹下面了,可以看到组件中主要就是两个文件,一个逻辑文件 src/components/CountDownButton/index.js ,还有一个样式文件 src/components/CountDownButton/index.css是,这里我们要做个小修改就是逻辑文件里面引用的是 index.scss 文件,我们需要一下这个样式文件的后缀为 index.scss

接着为了和我们的现有的 UI 统一,我们还改了 src/components/CountDownButton/index.scss文件的 activeButtonStylebuttonCommonStyle 样式,最后的文件内容如下:

/* 按钮默认展现样式 */

.buttonCommonStyle {
  margin: 0;
  width: 160px;
  height: calc(1.4rem + 20px);
  padding: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  line-height: 1;
  border-radius: 4px;
  border: none;
  outline: none;
}
/* 禁用时候的TouchableOpacity样式 */

.disableButtonStyle {
  background-color: #f6f6f6 !important;
}
/* 可以点击时候的TouchableOpacity样式 */

.activeButtonStyle {
  background-color: #02b875;
}
/* 文本默认样式 */

.txtCommonStyle {
  font-size: 20px;
}
/* 禁用时Text样式 */

.disableTxtStyle {
  color: #999;
}
/* 可以点击时候的Text样式 */

.activeTxtStyle {
  color: #fff;
}

大功告成!🥳,我们成功的完成了新版普通登录的界面,当你保存代码,并在根目录下通过 npm run dev:weapp 开启微信小程序,并使用开发者工具打开我们的项目时,它的效果应该类型下面这样:

怎么样,是不是和你之前体验的各种专业 App 或者网站、小程序的登录注册界面和逻辑类似了呢?😆,有了这样一个专业的登录界面之后,我们接下来将要把它整个从前端到后端的逻辑跑通,下一步我们将跑通这个登录和注册逻辑。

提示 这里我们将登录和注册页面整合在了一起,通过在登录按钮下方的小文字提示,如果用户没有账号,那么通过手机号和验证码登录之后,我们会为用户直接注册一个账号,而简化界面逻辑的背后通常需要在代码逻辑上做出大量的改进、优化等,然而我们在下一步即将接入的通用的身份云平台 -- Authing 则将这一逻辑简化到了几行代码。

使用 Authing 接入完整的用户系统

在文章开头和上一小结末尾买了那么多关子,说 Authing 如何简化我们的开发成本,有些读者估计都有点急不可耐了,这个 Authing 有这么方便嘛?,我们这一节就来开始深入使用它。

为了将 Authing 接入我们的博客小程序,我们需要做以下几点准备:

  • 注册 Authing 账号并创建一个 “小程序” 类型的用户池
  • 通过官方文档,找到小程序 SDK,并下载对于的文件放置到项目目录下
  • 在项目代码中导入 Authing 小程序 SDK,并开始使用

注册 Authing 账号

打开 Authing 官网,我们会看到如下界面:

image.png

我们点击右上角的登录,可以看到,它会弹出如下界面:

我们这时候可以慢下脚步,好好看一下提供通用身份云平台的公司,他们的登录界面是怎么样的呢?可以看到,我们熟悉的微信登录、邮箱+密码登录、手机号+验证码登录、还有技术开发者常用的 Github 登录,甚至还有一个比较特殊的小程序扫码登录,基本将互联网上我们可能用到的最高效率的用户登录、注册功能逻辑都集成进了一个小小的表单里面。

读者可以自行选择自己喜欢的登录方式😋,这里图雀酱选择了微信登录,然后在弹出的扫码界面,使用微信扫码二维码登录。登录之后,会弹出一个界面让你绑定手机号,读者这里可以自行选择是否绑定。当完成了这一步操作之后,界面会导航带你来到一个创建应用的界面,我们选择小程序,然后点击下一步:

接着,会问你的应用是干什么的,我们填入:“图雀社区博客小程序”(读者这里可以自行脑补),然后我们点击下一步:

接着会让你设置一个二级域名,我们输入 tuture-blog-miniprogram,然后我们点击完成:

接下来我们回来到一个快速体验 Authing 功能的界面,系统为你默认创建了一个账号:

  • 账号:test@test.com
  • 密码:123456a!

你可以在右边的界面里面体验是否可以登录,当然你也可以注册一个用户,注册的用户稍后我们可以在控制台我们创建的 “图雀社区博客小程序” 用户池里面看到这个注册的用户:

并且上面的界面还讲解了如何快速集成  Authing 的登录功能和检查登录状态,并给出了 JS 实现代码,以及左下角的 Guard ,这个 Guard 简单来说就是一个集合了我们之前看到的 Authing 那个注册、登录表单的功能,并且提供一个专业的界面给你,使得你可以几行代码就实现一个类似 Authing 官方注册登录的那个样子。也就是我们刚刚看到的这个界面:

提示 我们在图雀社区的全栈电商系列文章的番外篇里面集成用户系统有讲到如何使用,感兴趣的读者可以阅读此篇文章。

好的,我们点击左下角的 “知道了,进入控制台”,开始进入我们的 Authing 用户池管理控制台,在此之前还会让你填写一个回调地址,这个我们暂时用不到,你可以跳过,或者可以随便填写一个,比如 http://localhost:3000

最后,我们来到了这样一个界面:

可以看到这个界面左侧就是我们之前一直提到的 “用户池” 管理界面,默认选中了我们刚刚创建的 “图雀社区博客小程序”,一个用户池就是一整套用户以及和用户登录、注册、鉴权、登录状态、活跃、权限等有关的逻辑。

中间就是单个用户池里面的一些介绍,比如我们现在看到的是一个类似 Github 那个热力图一样的用户登录热力图,你可以方便的看到那天有多少次用户登录,往下滑可以看到更多用户数据分析方面的图表和内容。

下载和配置 SDK

注册完账号、建立了用户池,我们需要下载 Authing 为我们提供的微信小程序 SDK 来集成用户系统,点击这个链接去往小程序开发文档。

根据文档,我们需要在微信小程序后台配置两个域名白名单:

  • [https://oauth.authing.cn](https://oauth.authing.cn)[https://users.authing.cn](https://users.authing.cn)

然后将微信小程序的 AppIdAppSecret 填入 Authing 对应的地址:

这个界面,然后滑动到底部,选择小程序内登陆:

在弹出的框里面填入对应的微信小程序的 AppIdAppSecret

配置好之后,我们接下来可以将 SDK 代码下载,并放进我们的项目里,找一个地方(非现有项目中),运行如下脚本,Clone 小程序 SDK:

$ git clone https://github.com/Authing/authing-wxapp-sdk

然后打开此项目,将其中的 authing 文件夹拷贝进我们 ultra-club 小程序的 src/utils 目录下,最后的目录结构看起来应该是这样:

src
├── store
│   └── index.js
└── utils
    └── authing
        ├── authing.js
        ├── configs.js
        ├── graphql
        │   └── wxgql.js
        └── utils
            ├── qiniuUploader.js
            ├── util.js
            └── wxapp_rsa.min.js

SDK 开发环境准备就绪✌️,我们接下来马上来集成手机号+验证码登录的身份逻辑!

开始集成

打开 src/components/LoginForm/index.jsx 文件,对其中的内容作出对应的修改如下:

import Taro, { useState, useRef, useEffect } from '@tarojs/taro'
import { View, Form } from '@tarojs/components'
import { AtButton, AtImagePicker } from 'taro-ui'
import { useDispatch } from '@tarojs/redux'

import { SET_IS_OPENED, SET_LOGIN_INFO } from '../../constants'
import CountDownButton from '../CountDownButton'
import Authing from '../../utils/authing/authing'
import './index.scss'

export default function LoginForm(props) {
  // Login Form 登录数据
  const [phone, setPhone] = useState('')
  const [phoneCode, setPhoneCode] = useState('')
  const countDownButtonRef = useRef(null)
  let userPoolId = ''

  const dispatch = useDispatch()

  async function countDownButtonPressed() {
    if (!phone) {
      Taro.atMessage({
        type: 'error',
        message: '您还没有填写手机!',
      })

      return
    }

    countDownButtonRef.current.startCountDown()

    try {
      const authing = new Authing({
        userPoolId,
      })
      const res = await authing.getVerificationCode(phone)

      if (res.code === 200) {
        Taro.atMessage({
          type: 'success',
          message: '手机验证码已经发送成功,注意查收',
        })
      }
    } catch (err) {
      Taro.atMessage({
        type: 'error',
        message: '验证码发送失败,请稍后尝试 !',
      })
      console.log('err', err)
    }
  }

  async function handleSubmit(e) {
    e.preventDefault()

    // 鉴权数据
    if (!phone || !phoneCode) {
      Taro.atMessage({
        type: 'error',
        message: '您还有内容没有填写!',
      })

      return
    }

    try {
      const authing = new Authing({
        userPoolId,
      })

      const userInfo = await authing.loginByPhoneCode({
        phone,
        phoneCode,
      })

      // 提示登录成功
      Taro.atMessage({
        type: 'success',
        message: '恭喜您,登录成功!',
      })

      const { nickname, photo, _id } = userInfo

      // 向后端发起登录请求
      await Taro.setStorage({
        key: 'userInfo',
        data: {
          nickName: nickname,
          avatar: photo,
          _id,
        },
      })

      await Taro.setStorage({
        key: 'token',
        data: userInfo.token,
      })

      dispatch({
        type: SET_LOGIN_INFO,
        payload: { nickName: nickname, avatar: photo, userId: _id },
      })

      dispatch({ type: SET_IS_OPENED, payload: { isOpened: false } })
    } catch (err) {
      Taro.atMessage({
        type: 'error',
        message: '登录失败',
      })
    }
  }

  return (
    <View className="post-form">
      <Form onSubmit={handleSubmit}>
        <View className="login-box">
          <Input
            className="input-phone input-item"
            type="text"
            placeholder="输入手机号"
            value={phone}
            onInput={e => setPhone(e.target.value)}
          />
          <View className="verify-code-box">
            <Input
              className="input-nickName input-item"
              type="text"
              placeholder="四位验证码"
              value={phoneCode}
              onInput={e => setPhoneCode(e.target.value)}
            />
            <CountDownButton
              onClick={countDownButtonPressed}
              ref={countDownButtonRef}
            />
          </View>
          <AtButton formType="submit" type="primary">
            登录
          </AtButton>
          <View className="at-article__info">
            通过手机和验证码来登录,如果没有账号,我们将自动创建
          </View>
        </View>
      </Form>
    </View>
  )
}

可以看到,上面的内容主要有如下几处修改:

  • 我们首先引入了上一步里面下载的 Authing  SDK
  • 接着我们定义了一个 userPoolId ,这就是我们前面创建 “图雀社区博客小程序” 用户池的标志 ID,这里读者需要前往 Authing 控制台界面,获取用户池 ID,并替换上面的空字符串:

  • 接着我们在 countDownButtonPressed 函数内进行发起手机验证码操作,我们首先使用 new Authing 传入用户池 ID userPoolId 初始化一个一个实例并命名为 authing ,这一步代表我们拿到了此页用户池的操作权,接下来我们就可以进行用户有关的操作了。
  • 接着我们使用 authing.getVerificationCode 方法,传入填写的手机号 phone ,它是一个异步 Promise 对象,所以我们用 await 关键字获取其结果,当结果 res.code 为 200,代表发送验证码成功,我们提示用户发送验证码成功,否则提醒发送验证码失败,当编写了上面的代码并保存之后,我们可以打开小程序尝试一下效果,输入手机号,并点击发送验证码:

当然上面的手机号我瞎输入的,读者请自行输入自己的手机号尝试,接着应该可以在手机上收到验证码短信:

Boom💥!可以看到简单几行代码,我们就搞定了手机验证码的发送。

  • 接下来我们需要完善一下使用手机+验证码登录的逻辑,我们在 handleSubmit 里面编写了一个 try/catch 语句,然后初始化 Authing 对象,并调用方法 authing.loginByPhoneCode 传入我们的手机号(phone )和验证码 phoneCode ,进行调用之后,我们就完成了手机号+验证码登录,这个方法默认会对未登录用户进行创建账号操作,不需要用户额外处理其他逻辑。
  • 接着,我们通过登录成功返回的 userInfo 拿到内容,做出修改并设置到 storage 里,以及存在  Redux Store 里面,并提示用户登录成功。当然如果登录失败,我们还会提示用户登录失败。

提示

  1. 这里我们做了数据格式的适应,如将 Authing 登录返回的用户信息 userInfo.nickname 适应成 nickName ,是为了匹配之前的小程序系统的数据格式。
  2. 可以看到我们额外存了一个 userInfo.tokenstorage 里面,这个 token 就是我们用户系统里面用于用户鉴权的标志,之后我们将用这个 token 来检查用户的登录状态并进行用户登录态的保持。

一切准备就绪,接下来我们填入手机号,点击获取验证码,并将验证码填入小程序的输入框,点击登录应该就可以登录成功:

可以看到,我们收获了一个默认的 “酷酷的头像”,并且提示了登录成功。大功告成,一个专业的只需要手机号+验证码的登录界面+逻辑我们就完整实现了,可以看到我们主要在界面的调整和 SDK的引入上废了一点功夫,实际上实现整个逻辑,真的只需要几行代码!因为 Authing 在背后做了大量的工作来确保上层逻辑的简单。

处理登出逻辑

在上一小节中,我们成功将登录逻辑迁移到了手机号+验证码的方式,并且通过简单几行代码实现了验证码的发送,以及登录。

因为我们的登录逻辑相比之前有了一些变化,所以我们要适当的调整我们的登出逻辑,以适应这些变化。

改进登出组件

打开 src/components/Logout/index.js 文件,对其中的内容做出对应的修改:

import Taro, { useState } from '@tarojs/taro'
import { AtButton } from 'taro-ui'
import { useDispatch, useSelector } from '@tarojs/redux'

import { SET_LOGIN_INFO } from '../../constants'
import Authing from '../../utils/authing/authing'

export default function LoginButton(props) {
  const [isLogout, setIsLogout] = useState(false)
  const dispatch = useDispatch()
  const userId = useSelector(state => state.user.userId)

  async function handleLogout() {
    setIsLogout(true)

    try {
      await Taro.removeStorage({ key: 'userInfo' })
      await Taro.removeStorage({ key: 'token' })

      const userPoolId = ''
      const authing = new Authing({
        userPoolId,
      })

      await authing.logout(userId)

      dispatch({
        type: SET_LOGIN_INFO,
        payload: {
          avatar: '',
          nickName: '',
          userId: '',
        },
      })
    } catch (err) {
      console.log('removeStorage ERR: ', err)
    }

    setIsLogout(false)
  }

  return (
    <AtButton type="secondary" full loading={isLogout} onClick={handleLogout}>
      退出登录
    </AtButton>
  )
}

可以看到,上面的内容主要有如下几处修改:

  • 我们在 handleLogout 函数里处理登出逻辑的时候,首先初始化了一个 authing 实例,主要这里的 userPoolId 也需要读者替换成你自己的,可以在 Authing 控制台获取,接着调用 authing.logout 传入用户的 userId 来登出此用户,这样之后就不能操作 Authing 上创建的用户池了
  • 关于 userId 的获取,我们使用了 react-redux 钩子 useSelector 从 Redux Store 里面获取。
  • 最后我们还要删除 storage 里面存储的 token

清理其他登出逻辑

因为目前我们的登陆不是之前的使用 nickNameavatar ,而是使用手机号+验证码,所以我们一登录之后默认的 nickName 为空,而我们之前的判断用户是否登录的组件逻辑都是判断 nickName 是否存在,这里就有问题了,所以我们需要修改一下。

打开 src/components/Footer/index.js 文件,对其中的内容作出对应的修改如下:

import Taro from '@tarojs/taro'
import { View } from '@tarojs/components'
import { AtFloatLayout } from 'taro-ui'
import { useSelector, useDispatch } from '@tarojs/redux'

import Logout from '../Logout'
import LoginForm from '../LoginForm'
import './index.scss'
import { SET_IS_OPENED } from '../../constants'

export default function Footer(props) {
  const userId = useSelector(state => state.user.userId)

  const dispatch = useDispatch()

  // 双取反来构造字符串对应的布尔值,用于标志此时是否用户已经登录
  const isLogged = !!userId

  // 使用 useSelector Hooks 获取 Redux Store 数据
  const isOpened = useSelector(state => state.user.isOpened)

  return (
    <View className="mine-footer">
      {isLogged && <Logout />}
      <View className="tuture-motto">
        {isLogged ? 'From 图雀社区 with Love ❤' : '您还未登录'}
      </View>
      <AtFloatLayout
        isOpened={isOpened}
        title="登录"
        onClose={() =>
          dispatch({ type: SET_IS_OPENED, payload: { isOpened: false } })
        }
      >
        <LoginForm />
      </AtFloatLayout>
    </View>
  )
}

可以看到,上面的内容主要就是将 nickName 替换成了 userId ,并用 userId 判断是否处于登录状态。

同样的,src/components/Header/index.js 也要作出类似的修改:

import Taro from '@tarojs/taro'
import { View } from '@tarojs/components'
import { AtMessage } from 'taro-ui'
import { useSelector } from '@tarojs/redux'

import LoggedMine from '../LoggedMine'
import LoginButton from '../LoginButton'
import WeappLoginButton from '../WeappLoginButton'
import AlipayLoginButton from '../AlipayLoginButton'

import './index.scss'

export default function Header(props) {
  const userId = useSelector(state => state.user.userId)

  // 双取反来构造字符串对应的布尔值,用于标志此时是否用户已经登录
  const isLogged = !!userId

  const isWeapp = Taro.getEnv() === Taro.ENV_TYPE.WEAPP
  const isAlipay = Taro.getEnv() === Taro.ENV_TYPE.ALIPAY

  return (
    <View className="user-box">
      <AtMessage />
      <LoggedMine />
      {!isLogged && (
        <View className="login-button-box">
          <LoginButton />
          {isWeapp && <WeappLoginButton />}
          {isAlipay && <AlipayLoginButton />}
        </View>
      )}
    </View>
  )
}

还有我们的 “我的” 页面,src/pages/mine/mine.jsx 文件:

import Taro, { useEffect } from '@tarojs/taro'
import { View } from '@tarojs/components'
import { useDispatch, useSelector } from '@tarojs/redux'

import { Header, Footer } from '../../components'
import './mine.scss'
import { SET_LOGIN_INFO } from '../../constants'

export default function Mine() {
  const dispatch = useDispatch()
  const userId = useSelector(state => state.user.userId)

  const isLogged = !!userId

  useEffect(() => {
    async function getStorage() {
      try {
        const { data } = await Taro.getStorage({ key: 'userInfo' })

        const { nickName, avatar, _id } = data

        // 更新 Redux Store 数据
        dispatch({
          type: SET_LOGIN_INFO,
          payload: { nickName, avatar, userId: _id },
        })
      } catch (err) {
        console.log('getStorage ERR: ', err)
      }
    }

    if (!isLogged) {
      getStorage()
    }
  })

  return (
    <View className="mine">
      <Header />
      <Footer />
    </View>
  )
}

Mine.config = {
  navigationBarTitleText: '我的',
}

最后,我们修改首页的 src/pages/index/index.jsx

import Taro, { useEffect } from '@tarojs/taro'
import { View, Text } from '@tarojs/components'
import { AtFab, AtFloatLayout, AtMessage } from 'taro-ui'
import { useSelector, useDispatch } from '@tarojs/redux'

import { PostCard, PostForm } from '../../components'
import './index.scss'
import {
  SET_POST_FORM_IS_OPENED,
  SET_LOGIN_INFO,
  GET_POSTS,
} from '../../constants'

export default function Index() {
  const posts = useSelector(state => state.post.posts) || []
  const isOpened = useSelector(state => state.post.isOpened)
  const userId = useSelector(state => state.user.userId)

  const isLogged = !!userId

  const dispatch = useDispatch()

  useEffect(() => {
    const WeappEnv = Taro.getEnv() === Taro.ENV_TYPE.WEAPP

    if (WeappEnv) {
      Taro.cloud.init()
    }

    async function getStorage() {
      try {
        const { data } = await Taro.getStorage({ key: 'userInfo' })

        const { nickName, avatar, _id } = data

        // 更新 Redux Store 数据
        dispatch({
          type: SET_LOGIN_INFO,
          payload: { nickName, avatar, userId: _id },
        })
      } catch (err) {
        console.log('getStorage ERR: ', err)
      }
    }

    if (!isLogged) {
      getStorage()
    }

    async function getPosts() {
      try {
        // 更新 Redux Store 数据
        dispatch({
          type: GET_POSTS,
        })
      } catch (err) {
        console.log('getPosts ERR: ', err)
      }
    }

    if (!posts.length) {
      getPosts()
    }
  }, [])

  function setIsOpened(isOpened) {
    dispatch({ type: SET_POST_FORM_IS_OPENED, payload: { isOpened } })
  }

  function handleClickEdit() {
    if (!isLogged) {
      Taro.atMessage({
        type: 'warning',
        message: '您还未登录哦!',
      })
    } else {
      setIsOpened(true)
    }
  }

  console.log('posts', posts)

  return (
    <View className="index">
      <AtMessage />
      {posts.map(post => (
        <PostCard key={post._id} postId={post._id} post={post} isList />
      ))}
      <AtFloatLayout
        isOpened={isOpened}
        title="发表新文章"
        onClose={() => setIsOpened(false)}
      >
        <PostForm />
      </AtFloatLayout>
      <View className="post-button">
        <AtFab onClick={handleClickEdit}>
          <Text className="at-fab__icon at-icon at-icon-edit"></Text>
        </AtFab>
      </View>
    </View>
  )
}

Index.config = {
  navigationBarTitleText: '首页',
}

修改了如上代码并保存之后,打开应用,我们点击登出,应该顺利看到如下效果:

小结

在这一节中,我们呼应使用 Authing 登录的逻辑,对应修改了登出逻辑,并且使用 userId 替换 nickName 作为是否登录的判断标准。

集成微信授权登录

在前两小节中,我们使用 Authing 集成了手机号+验证码的登录逻辑,然后处理了登出逻辑,有同学可能会问了,我们之前是取代了普通登录,还有一个微信登录,我们是不是也可以用 Authing 来进行替换呢?毕竟集成用户系统肯定要全面集成,答案是可以!

接下来我们将使用 Authing 为我们提供的 loginWithWxapp ,快捷的将微信授权登录集成好,打开 src/components/WeappLoginButton/index.js 文件,对其中的内容作出对应的修改如下:

import Taro, { useState, useEffect } from '@tarojs/taro'
import { Button } from '@tarojs/components'
import { useDispatch } from '@tarojs/redux'

import './index.scss'
import { SET_LOGIN_INFO } from '../../constants'
import Authing from '../../utils/authing/authing'

export default function WeappLoginButton(props) {
  const [isLogin, setIsLogin] = useState(false)

  const dispatch = useDispatch()

  async function onGetUserInfo(e) {
    setIsLogin(true)

    const userPoolId = '5ea4ffa72b3a80b6eff60b65'
    const authing = new Authing({
      userPoolId,
    })

    async function loginWithAuthing(code, detail) {
      try {
        const userInfo = await authing.loginWithWxapp({
          code,
          detail,
        })

        // 提示登录成功
        Taro.atMessage({
          type: 'success',
          message: '恭喜您,登录成功!',
        })

        const { nickname, photo, _id } = userInfo

        dispatch({
          type: SET_LOGIN_INFO,
          payload: { nickName: nickname, avatar: photo, userId: _id },
        })

        // 向后端发起登录请求
        await Taro.setStorage({
          key: 'userInfo',
          data: {
            nickName: nickname,
            avatar: photo,
            _id,
          },
        })

        await Taro.setStorage({
          key: 'token',
          data: userInfo.token,
        })

        // 当 code 用于登录之后,会失效,所以这里重新获取 code
        Taro.login({
          success(res) {
            const code = res.code
            Taro.setStorageSync('code', code)
          },
        })
      } catch (err) {
        console.log('err', err)
        Taro.atMessage({
          type: 'success',
          message: '恭喜您,登录成功!',
        })
      }
    }

    try {
      const code = Taro.getStorageSync('code')
      Taro.login({
        success(res) {
          const code = res.code
          loginWithAuthing(code, e.detail)
        },
      })
    } catch (err) {
      console.log('err', err)
    }

    setIsLogin(false)
  }

  return (
    <Button
      openType="getUserInfo"
      onGetUserInfo={onGetUserInfo}
      type="primary"
      className="login-button"
      loading={isLogin}
    >
      微信登录
    </Button>
  )
}

可以看到上面的内容主要有如下几处修改:

  • 我们删除了之前简单粗暴获取到 userInfo 里面的 nickNameavatarUrl 就发起登录的代码逻辑。
  • 我们在 onGetUserInfo 里面初始化了一个 authing 实例,然后定义了一个 loginWithAuthing 方法,具体细节我们马上讲解,然后我们使用 Taro.login 调用微信授权登录 API,获取对应的 code ,并连同把 onGetUserInfo 传进来的 e.detail 一起传给 loginWithAuthing
  • loginWithAuthing 函数里面,哦们首先调用 authing.loginWithWxapp ,并传入对应的 codedetail ,进行登录,然后将登录获取的信息存在 storage 里面以及保存在 Redux Store 中,并提示用户登录成功。

保存上面的代码,并运行我们的应用,你应该可以自由的操作微信登录了:

就这样,我们就成功将微信授权登录使用 Authing 集成好了,可以看到我们只需要一个 loginWithWxapp 就把逻辑集成好了,完全不需要之前 dispatch 一个 LOGIN 请求,还要去处理一堆 sagas 逻辑,并且还要编写小程序云函数逻辑,手动处理这些逻辑不仅繁琐,还容易出错,并且也不够灵活,而 Authing 提供的 SDK 可以很好的解决这一点,赋能业务成功。

新版用户系统整合进现有后端

在之前四个小节,我们都在将现有小程序博客的用户逻辑使用 Authing 来替代,而将用户逻辑用 Authing 来替代之后,我们会遇到一个小问题,就是之前的用户系统与其他模块如我们的发帖模块是存在耦合的,所以我们还需要将这个耦合的部分替换成 Authing 的相关逻辑,这就涉及到如何将新版的用户系统整合进现有的后端。

我们目前的博客小程序涉及到和用户系统耦合的部分就是我们云函数 createPost 在发帖的时候要带上用户信息,所以我们需要在这个云函数下使用 Authing 来替换相应的用户逻辑。

安装 SDK

我们的微信小程序后台使用了云函数,而云函数是一个个的 Node.js 函数,而 Authing 为我们提供了 Node.js 的 SDK npm 包,我们马上来安装它,在 functions/createPost 下执行如下的代码:

$ npm install authing-js-sdk

执行之后,我们的 package.json 会是如下的样子:

{
  "name": "createPost",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "authing-js-sdk": "^3.18.7",
    "wx-server-sdk": "latest"
  }
}

接着,我们在云函数里面替换对应的逻辑,打开 functions/createPost/index.js 文件,对其中的内容做出对应的修改如下:

// 云函数入口文件
const cloud = require('wx-server-sdk')
const Authing = require('authing-js-sdk')

cloud.init({
  env: cloud.DYNAMIC_CURRENT_ENV,
})

const db = cloud.database()

// 云函数入口函数
exports.main = async (event, context) => {
  const { postData, userId } = event

  const userPoolId = ''
  const secret = ''

  try {
    const authing = new Authing({
      userPoolId,
      secret,
    })
    const userInfo = await authing.user({ id: userId })
    const { nickname, photo } = userInfo

    const { _id } = await db.collection('post').add({
      data: {
        ...postData,
        user: { nickName: nickname, avatar: photo, _id: userInfo._id },
        createdAt: db.serverDate(),
        updatedAt: db.serverDate(),
      },
    })

    const newPost = await db
      .collection('post')
      .doc(_id)
      .get()

    return {
      post: { ...newPost.data },
    }
  } catch (err) {
    console.error(`createUser ERR: ${err}`)
  }
}

可以看到,我们主要做了如下几处修改:

  • 我们导入了 Authing SDK
  • 然后函数内部我们定义了 userPoolIdsecret 来初始化 authing 实例,这两个参数我们可以在用户池控制台找到:

  • 接着我们使用初始化好的 authing 来调用 authing.user 方法传入我们接收到的 userId 查询在 Authing 中保存的此用户资料,并用这个用户资料替换我们需要在小程序云数据库里面查到的用户数据

自此,我们就在前后端深度整合了 Authing 用户系统,在之后我们的应用扩展过程中,所有和用户有关的逻辑都不需要自己在后台单独编写,前端也大大简化了工作量,并且我们还能在 Authing 的控制台可视化用户的数据:登录情况、登录区域、登录机器,还可以给用户进行权限分配,甚至直接修改用户资料等。

通过鉴权保有用户登录状态

最后,我们来收尾一下,做一下用户登录状态的查询,因为应用的登录凭证它存在一个失效时间,当时间一到,我们再去操作用户信息就会显示没有权限,因为凭证失效了,所以说我们要及时检查用户的登录凭证是否失效,如果失效则要求用户重新登录,这也是读者经常会在访问某些网站的时候遇到,而现在我们将实操一下这个过程。

一般处理用户登录态的验证主要是在应用刚刚启动时,去进行一个鉴权处理,如果用户态有效,则顺利从应用的 storage  里面取出数据,然后设置进前端状态管理,进而展示用户数据,而如果没有则删除 storage 里面的数据,提示用户进行登录。

我们打开 src/pages/index/index.jsx 来实操,对其中的内容作出对应的修改如下:

import Taro, { useEffect } from '@tarojs/taro'
import { View, Text } from '@tarojs/components'
import { AtFab, AtFloatLayout, AtMessage } from 'taro-ui'
import { useSelector, useDispatch } from '@tarojs/redux'

import { PostCard, PostForm } from '../../components'
import './index.scss'
import {
  SET_POST_FORM_IS_OPENED,
  SET_LOGIN_INFO,
  GET_POSTS,
} from '../../constants'
import Authing from '../../utils/authing/authing'

export default function Index() {
  const posts = useSelector(state => state.post.posts) || []
  const isOpened = useSelector(state => state.post.isOpened)
  const userId = useSelector(state => state.user.userId)

  const isLogged = !!userId

  const dispatch = useDispatch()

  useEffect(() => {
    const WeappEnv = Taro.getEnv() === Taro.ENV_TYPE.WEAPP

    if (WeappEnv) {
      Taro.cloud.init()
    }

    async function getStorage() {
      // 在应用初始化的时候,对应用进行鉴权,检查登录状态,如果登录失效,则情况缓存信息
      try {
        const userPoolId = ''
        const { data: token } = await Taro.getStorage({ key: 'token' })
        const authing = new Authing({
          userPoolId,
        })
        const result = await Taro.request({
          url: `https://users.authing.cn/authing/token?access_token=${userInfo.token}`,
        })

        if (result.data.status) {
          const { data } = await Taro.getStorage({ key: 'userInfo' })

          const { nickName, avatar, _id } = data

          // 更新 Redux Store 数据
          dispatch({
            type: SET_LOGIN_INFO,
            payload: { nickName, avatar, userId: _id },
          })
        } else {
          await Taro.removeStorage({ key: 'userInfo' })
          await Taro.removeStorage({ key: 'token' })
        }
      } catch (err) {
        console.log('getStorage ERR: ', err)
      }
    }

    if (!isLogged) {
      getStorage()
    }

    async function getPosts() {
      try {
        // 更新 Redux Store 数据
        dispatch({
          type: GET_POSTS,
        })
      } catch (err) {
        console.log('getPosts ERR: ', err)
      }
    }

    if (!posts.length) {
      getPosts()
    }
  }, [])

  function setIsOpened(isOpened) {
    dispatch({ type: SET_POST_FORM_IS_OPENED, payload: { isOpened } })
  }

  function handleClickEdit() {
    if (!isLogged) {
      Taro.atMessage({
        type: 'warning',
        message: '您还未登录哦!',
      })
    } else {
      setIsOpened(true)
    }
  }

  console.log('posts', posts)

  return (
    <View className="index">
      <AtMessage />
      {posts.map(post => (
        <PostCard key={post._id} postId={post._id} post={post} isList />
      ))}
      <AtFloatLayout
        isOpened={isOpened}
        title="发表新文章"
        onClose={() => setIsOpened(false)}
      >
        <PostForm />
      </AtFloatLayout>
      <View className="post-button">
        <AtFab onClick={handleClickEdit}>
          <Text className="at-fab__icon at-icon at-icon-edit"></Text>
        </AtFab>
      </View>
    </View>
  )
}

Index.config = {
  navigationBarTitleText: '首页',
}

可以看到,上面的内容主要做出了如下几处修改:

  • 我们在 getStorage 函数里面,首先获取了之前登录时保存的用户凭证 token ,然后初始化了一个 authing 实例,并通过 Taro.request 的方式,去请求 Authing 为我们提供的鉴权地址:[https://users.authing.cn/authing/token?access_token=YOUR_TOKEN](https://users.authing.cn/authing/token?access_token=YOUR_TOKEN) ,我们将这个链接中的 YOUR_TOKEN 替换成我们保存在 storage 里面的 token ,访问这个地址如果成功则会得到如下的结果:
{
  "status": true,
  "message": "已登录",
  "code": 200,
  "token": {
    "data": {
	"email": "YOUR_EMAIL@domain.com",
	"id": "YOUR_USER_ID",
	"clientId": "YOUR_UESR_POOL_ID"
    },
        "iat": "Token 签发时间"
	"exp": "Token 过期时间"
    }
}

如果失败其中的 status 会为 false ,其它内容也会相应的变化。

  • 接着我们判断 status ,如果为 true 则从 storage 里面取出数据,设置进 Redux Store,如果为 false ,我们清空 storage 数据,这样在用户发帖时会提示用户需要登录。

小结

通过这篇教程,我们将之前一个比较简单的用户系统替换成了专业的通用云身份提供商 Authing 提供的专业的用户系统,并且体验到了通过短短几行代码就可以实现专业的手机号+验证码登录、用户登出、微信授权登录、并且还可以做用户登录状态的检测等。

有了这样一个简单、方便且强大的用户系统做保障之后,我们的博客应用小程序将可以无顾虑的扩展其它模块,而涉及到身份相关的内容都可以交给 Authing 来做。当然我们这片文章还只用了 Authing 很小的一部分功能,还有诸如企业组织管理、单点登录等高级功能,有兴趣的用户可以自行发掘💪!

想要学习更多精彩的实战技术教程?来图雀社区逛逛吧。