微信小程序手机号授权(Taro+云开发实现)

1,809 阅读6分钟

本文不重点讲云开发相关知识,读者需要对云开发有过接触,有一点的云开发基础,非常适合正在做小程序云开发需要解决登录问题以及在手机号授权方面有困惑的同学。

一、登录 VS 手机号授权

不能将微信小程序登录和手机号授权搞混,首先明确概念:在微信小程序中,OpenID是一个重要的概念。它主要用于用户的身份验证,并作为用户在小程序中的唯一标识符。

对于微信小程序,登录的一般流程是:

  1. 调用 wx.login 获取临时登录凭证 code。
  2. 将 code 发送到开发者服务器。
  3. 开发者服务器使用 code 调用微信登录凭证校验接口,获取 session_key 和 openid。
  4. 开发者服务器根据 session_key 和 openid 生成用户的自定义登录态,并返回给小程序端。
  5. 小程序端保存自定义登录态标志,之后可以根据登录标识来识别用户是否登录以及登录态是否过期。

这个过程可以在微信小程序启动时静默完成, wx.login 不需要授权,不会对用户造成干扰。在服务端登录成功后,就可以在数据库用户表初步维护用户信息。

对于微信小程序,手机号授权需要经过用户授权同意才能调用,用户同意后,小程序才可获得由平台验证后的手机号,进而维护进用户表中。自2023年8月28日起,手机号快速验证组件将需要付费使用。标准单价为:每次组件调用成功,收费0.03元(每个小程序账号将有1000次体验额度,用于开发、调试和体验)。开发者在获得 bindgetphonenumber 事件的 success 回调信息时会进行扣费。

因此,通过静默登录可以拿到登录标志,以及查询到用户表中该用户的信息,进而可以得知该条用户信息是否有手机号,如果没有则开放手机号授权,已经有了则不需要重复走手机号授权逻辑,因为这样既要重复扣费又会影响用户体验。

Tips: 建议手机号授权功能单独做一个登录页面,方面管理

二、微信小程序云开发

以上登录流程,用云开发来做,能不能做,答案是肯定的的,步骤见下:

  1. 前端调用 wx.login 获取 code
    小程序前端首先调用 wx.login 获取 code,这个 code 是小程序端调用微信登录凭证校验接口的临时票据。
  2. 前端将 code 发送到云函数
    前端将获取到的 code 发送到云函数,云函数在后端进行登录凭证的校验。
  3. 云函数调用微信 API 进行登录凭证校验
    云函数接收 code 后,通过微信官方提供的 code2Session 接口,将 code 发送到微信服务器,换取 openid 和 session_key。
  4. 云函数处理用户信息并返回
    云函数可以将 openid 和 session_key 存储到数据库(如果需要的话),并返回给前端一个自定义的登录凭证(如 token),用于后续的用户身份验证。
  5. 前端保存登录凭证
    前端接收到云函数返回的登录凭证后,保存到本地(如使用 wx.setStorageSync),用于后续请求的验证。

image.png

可是,既然使用了云开发,云开发的优势,开发者无需搭建服务器,可免登录、免鉴权直接使用平台提供的 API 进行业务开发,那么不就剩下手机号授权了嘛。

三、手机号授权+云函数

使用小程序云开发免登录、免鉴权,那么要在这里实现手机号授权登录,其实就是手机号授权,往users集合里添加一条用户记录,有了用户记录,视作登录

思路如下:

1、对于初次进入小程序的用户,任何涉及到用户登录的操作,在本地未检测到用户信息,则跳转到登录页面处理;

2、在登录页面完成手机号授权请求,调用云函数;

3、云函数接收到请求,处理请求,拿到手机号;(OpenId 作为用户在小程序中的唯一标识,在云函数里可以直接拿得,通过 code 和 OpenId 获取用户手机号)

4、根据 OpenId 查询用户信息,没有则插入一条数据并返回该条数据,有则直接返回用户数据

5、保存用户数据到缓存

6、下次进到小程序,在小程序启动时,防止用户清理缓存,或者涉及到用户的其它业务处理,重新获取用户信息,更新到缓存

<Button 
type="primary" 
openType="getPhoneNumber" 
onGetPhoneNumber={(e) => getPhoneNumber(e)}
> 
手机号授权登录
</Button>

  

const getPhoneNumber = (e) => {
    // e.detail 包含的值
    {
  "errMsg": "getPhoneNumber:ok",
  "encryptedData": "JuGuYc2fDeZ/+EstZRgEAsNQjBdr4R8aOP5htlGXyJkCmYEDUR/S7mOYeTASXz0JQXua+hbTOx29+ddSeDc56RPhWu9uoBiMVSOsUAp9gzrw7O38+JQHJ+Hkv/MDEPh/v3S+7nZtedlL+CA2zlRTsnvQrw8A5AD1sms7mrwwh68hsTFZhao2s0O6kgHZea3DEudElm0R6PiCLYo391gjIw==",
  "iv": "XYv66LBZqOvoegGCd+1SQg==",
  "cloudID": "80_qjRVVLg7vkgG5kdPjWMa2LQGcTSYDsKJDcaS19jWIs0pYMwAt1s4n1KOeo0",
  "code": "8b515f39ff1543953e83916dd60d61448da4489ee5d1d8e6e4d16518d80e4e16"
    }
    
    if (e.detail.code) {
      Taro.cloud.init()
      Taro.cloud.callFunction({
        name: 'users',
        data: {
          type: 'grant_phone',
          code: e.detail.code
        },
        success: ({ result }: any) => {
          const { success, message, data } = result;
          if (success === 'ok') {
            Taro.showToast({
              title: '登录成功',
              icon: 'none'
            })
            // 刷新用户信息,记录在本地
          } else {
            Taro.showToast({
              title: message,
              icon: 'none'
            })
          }
        },
        fail: () => {
          Taro.showToast({
            title: '登录失败',
            icon: 'none'
          })
        }
      })
    } else {
      Taro.showToast({
        title: '获取手机号失败',
        icon: 'none'
      })
    }

}
// 云函数入口文件
const cloud = require('wx-server-sdk')
cloud.init({
  env: cloud.DYNAMIC_CURRENT_ENV
}) // 使用当前云环境
// 获取数据库
const db = cloud.database()
// 云函数入口函数
exports.main = async (event, context) => {
  const {
    code
  } = event
  const {
    OPENID
  } = cloud.getWXContext()
  try {
    const {
      phoneInfo = {}
    } = await cloud.openapi.phonenumber.getPhoneNumber({
      code,
      openid: OPENID
    })
    const {
      phoneNumber = ''
    } = phoneInfo

    // 获取用户数据集合
    const Users = db.collection("users")
    const {
      data = []
    } = await Users.where({
      openid: OPENID
    }).get()
    // 未注册, 则插入一条数据
    if (data.length <= 0) {
      Users.add({
        data: {
          openid: OPENID,
          name: phoneNumber.slice(-4) + '用户',
          phone: phoneNumber,
          ...
        }
      }).then((res) => {
        return {
          code: 0,
          message: 'succcess',
          data: res
        }
      })
    } else {
      return {
        code: 0,
        message: 'succcess',
        data: data[0]
      }
    }
  } catch (error) {
    return {
      success: 'no',
      message: error || 'error'
    }
  }
}

// app.tsx
useLaunch(() => {
    Taro.cloud.init({
      // env 参数决定接下来小程序发起的云开发调用(wx.cloud.xxx)会默认请求到哪个云环境的资源
      env: 'XXX',
      // 是否在将用户访问记录到用户管理中,在控制台中可见,默认为false
      traceUser: false,
    });
    
   Taro.cloud.callFunction({
    name: 'users',
    data: {
      type: 'check_user'
    },
    complete: ({
      result
    }) => {
      if (result?.data) {
        // ...
      } else {
        // ...
      }
    }
  })
 })

// 云函数入口文件
const cloud = require('wx-server-sdk')
// 使用当前云环境
cloud.init({
  env: cloud.DYNAMIC_CURRENT_ENV
})
// 获取数据库
const db = cloud.database();
// 云函数入口函数
exports.main = async (event, context) => {
  const {
    OPENID
  } = cloud.getWXContext()
  try {
    const Users = db.collection("users")
    const {
      data = []
    } = await Users.where({
      openid: OPENID,
    }).get()
    // 如果查到数据说明,用户登陆注册过
    if (data.length) {
      const userInfo = data[0];
      // 判断是否为会员,并且会员是否过期
      const isVip = await checkVipValidate(userInfo)
      return {
        data: {
          ...userInfo,
          userId: userInfo._id,
          isVip
        },
        code: 200,
        msg: '成功'
      }
    } else {
      // 用户未登录注册过
      return {
        data: {},
        code: 4000,
        msg: '未找到用户'
      }
    }
  } catch (error) {
    return {
      data: {},
      code: 4000,
      msg: error.message
    }
  }
}

文档

小程序登录:developers.weixin.qq.com/miniprogram…

手机号授权: developers.weixin.qq.com/miniprogram…

access-token:developers.weixin.qq.com/miniprogram…

检验登录态:developers.weixin.qq.com/miniprogram…