社区/学习指南/小程序·云开发高级教程

自定义登录

匿名登录可以让用户在无需注册登录的情况下在短期内使用数据库、云存储以及调用云函数,但是大多数的应用是需要获取用户的身份才能更长期更安全将数据存储在云端,并且可以获取跨设备跨端的一致性的体验。在 Web 端我们可以使用自定义登录与匿名登录相结合;在微信小程序端,借助于云开发,无需额外操作便可免鉴权登录(实际上就是 openId),要实现跨端一致,就需要考虑免鉴权登录与自定义登录相结合。

14.7.1 自定义登录与云开发

微信小程序云开发有一套免鉴权的账号体系 openid,我们可以基于这套账号体系结合自定义登录实现 Web 端和小程序端的跨端登录和权限控制。

1、创建一个用户集合和记录

为了学习的方便,我们先假定(或者模拟)用户已经使用过我们的小程序并留有 userId 和 openid,打开小程序云开发数据库在数据库里新建一个集合集合的名称为 users,在 users 里新建一个记录,比如:


{

  _openid:"oUL-m5FuRmuVmxvbYOGnXbnEDsn8",

  userId:"lidongbbsky",

}

由于小程序使用的是云开发,用户无需注册登录就可以调用云开发环境里的资源,那当这个用户到 Web 网页上时,应该怎么样才能登录以前的账号呢?只需要在网页上输入 userId 即可登录。

直接输入上面这个 userId 不输入密码就可以登录,这是一个不安全的做法,不过安全的做法也不一定需要密码,我们可以使用云函数每隔十几秒动态刷新 userId 来取代用户名+密码这种传统方式,比如 userId 为 openid 的后三位+只有 10 几秒生命周期的动态三位数(有点类似于短信的动态验证码),而用户 userId 的获取只能登录到小程序来获取,这样用户只需要输入 6 位数,既方便且安全。当然你也可以用其他方式来生成 userId。

通过数据库,我们把 userId 和小程序的唯一 openid 关联到了一起,那在 web 网页上又是怎样实现 userId 的登录呢?又是如何保证登录的安全性的呢?

2、获取私钥并编写 ticket 创建模块

打开腾讯云云开发网页控制台,在【环境】-【环境设置】-【登录方式】,单击私钥下载,私钥是一份 JSON 格式的数据,里面包含private_key_idprivate_key。接下来我们会用云函数把 openid 生成唯一用户 ID(称之为 customUserId)结合这个私钥文件计算出云开发的自定义登录凭证 ticket,最后使用 ticket 登录。

然后使用 VS Code 新建一个云函数比如 weblogin 云函数专门用来处理网页的登录,将私钥 json 文件的名称自定义一下,比如 tcb_custom_login.json 保存到与云函数的目录里。


├── weblogin  //weblogin云函数目录

│   └── index.js

│   └── config.json

│   └── package.json

│   └── tcb_custom_login.json //下载的私钥json文件

然后再在 index.js 里输入如下代码,创建一个生成 ticket 的服务,代码的逻辑如下:

  • 首先会获取用户在 web 页面填写的 userId,如果这个 userId 非空,我们就去数据库查询这个 userId 是否存在;

  • 如果 userId 存在,说明用户填写的 userId 是对的;

  • 查询这个用户的 openid,openid 是用户的唯一 ID,但是 customUserId 里不能有特殊有特殊符号,所以我们会把去掉 openid 的连接符作为 customUserId;

  • 然后用 createTicket 让 customUserId 再来结合密钥生成 ticket,而且这个 ticket 是每隔 10 分钟会刷新;

  • 再把 ticket 以集成请求的方式发送给 web 端,这样 web 端再来根据这个 ticket 来登录


const tcb = require('@cloudbase/node-sdk')

const app = tcb.init({

  env: 'xly-xrlur',

  credentials: require('./tcb_CustomLoginKeys.json')

})

const db = tcb.database();


exports.main = async (event, context) => {

  const userId = event.queryStringParameters.userId //从web端传入的userId

  try{

    if( userId != null){  //如果web端传入的userId非空,就从数据库查询是否存在该userId

      const users = (await db.collection('users').where({

        userId:userId

      }).get()).data



      if(users.length != 0){  //当数据库存在该userId时,users为一个数组,数组长度不为0

        //使用用户的openid为customUserId来生成ticket,因为openid有一个-连接符,把它给替换掉

        const customUserId = await (users[0]._openid).replace('-','')

        const ticket = app.auth().createTicket(customUserId, {

          refresh: 10 * 60 * 1000 // 每十分钟刷新一次登录态, 默认为一小时

        });

        return {

          statusCode: 200,

          headers: {

            'content-type': 'application/json',

            'Access-Control-Allow-Origin':'*',

            'Access-Control-Allow-Methods':'*',/=

            'Access-Control-Allow-Headers':'Content-Type'

          },

          body: ticket

        }

      }

    }

  }catch(err){

    console.log(err)

  }

}

将 weblogin 云函数部署上传之后,然后开启云接入(HTTP 触发)并创建路由比如/weblogin,我们可以在浏览器里输入以下地址(也就是在 weblogin 云接入里传入参数 userId 的值为 lidongbbsky)获取到生成的 ticket:


http://xly-xrlur.service.tcloudbase.com/weblogin?userId=lidongbbsky

3、web 前端根据 ticket 登录

我们已经使用云函数生成了一个 ticket,那前端又如何根据这个 ticket 来登录呢?我们还是使用 axios 进行 HTTP 请求,所以在我们的前端页面,比如 public 文件夹下的 index.html 里先引入 axios


<script src="https://imgcache.qq.com/qcloud/tcbjs/1.5.1/tcb.js"></script>

<script src="https://unpkg.com/axios/dist/axios.min.js"></script>

<script src="./js/main.js"></script>

然后再在 main.js 里的页面生命周期函数window.onload= function(){//生命周期函数}里输入以下代码,首先返回用户的登录态 LoginState 来判断用户是否已经登录,如果用户没有登录,则发起 HTTP 请求,获取云接入返回的 ticket,然后使用auth.customAuthProvider().signIn(ticket)用自定义登录凭证 ticket 来登录云开发:

const auth = app.auth({
  persistence: "session", //在窗口关闭时清除身份验证状态
});

async function login() {
  const loginState = app.auth().hasLoginState();

  if (!loginState) {
    const url = "https://xly-xrlur.service.tcloudbase.com/weblogin";

    axios
      .get(url, {
        userId: "lidongbbsky",
      })

      .then((res) => {
        auth
          .customAuthProvider()

          .signIn(res.data)

          .then(() => {
            console.log("登录成功");

            //登录成功后,就可以操作云开发环境里的各种资源啦
          })

          .catch((err) => {
            console.log("登录失败", err);
          });
      })
      .catch((err) => {
        console.log(err);
      });
  } else {
    console.log("您已经登录啦");
  }
}

login();

14.7.2 web 端账号与账号的打通

1、如何获取 web 端 openid(uid)

当我们在 web 端登录了之后,web 端用户也会一个类似于小程序的 openid(但是不相同),那我们要如何获取到这个 openid 呢?和小程序用户一样,当我们往云存储和数据库里添加数据时,就会自动添加一个 openid 的字段,里面的值就是 web 端 openid(uid)。

那除此之外,我们是否能够像小程序云开发一样在云函数里获取到 web 端用户的 openid 呢?这个其实我们已经在前面 web 端云开发里的 webtest 云函数就已经写了方法啦,这里再单独拿出来:

const tcb = require("@cloudbase/node-sdk");

const app = tcb.init({
  env: "xly-xrlur",
});

const auth = app.auth();

exports.main = async (event, context) => {
  const { openId, uid, customUserId } = auth.getUserInfo();

  return { openId, uid, customUserId };
};

这里的 uid 就是 web 端用户的 openid,而 openId 则是微信用户(小程序)的 openid,customUserId 就是前面我们用于生成 ticket 的 customUserId。

2、web 端和小程序 openid 的区别与联系

当用户在 web 端使用 customUserId 自定义登录之后也会有一个不同于小程序账户体系的 openid,这个 openid 是用户的 uid,customUserId 和 uid 是对应的,只要 customUserId 不变,web 端用户的 openid(uid)也不会变更。也就是说由于我们的 customUserId 是根据小程序的 openid 生成的唯一且不随设备不随时间变更而变更的,那么 web 端的 openid(uid)也不会因为设备和时间而变更。

尽管用户在 web 端传入的 userId 是可以动态刷新的,但是在云函数里我们并没有把这个可以动态刷新的 userId 作为 customUserId,所以不必担心 userId 的不同,web 端用户在云开发的 openid 会有所变化;ticket 也是可以动态刷新的,但是这只是加强账号的安全性,并不会影响 web 端用户的 openid 的唯一性。

web 端用户的 openid(也就是 uid)的唯一性,且不随设备和时间的变更而变更的永久性是我们可以进行跨设备操作的基础。不过值得一提的是,即使是相同用户 web 端的 openid 和小程序的 openid 虽然有关联,但是两者之间是不同的账号体系,如果我们要把小程序和 web 端的账号打通则需要进行一定的处理。

3、小程序端和 Web 端账号打通

即使是相同的用户,web 端和小程序端的 openid 都是唯一且永久的,而且都还不同,那如果让相同的用户在 Web 端和小程序端有一致性的体验和相同的权限呢?我们知道云开发的权限是非常依赖 openid的,无论是数据库的增删改查,还是云存储的增删改查,都是根据 openid 来判断用户的权限的。账号体系打通可能比较容易,但是权限又该如何控制呢?

比如用户在小程序端创建了个人资料,发表了一篇文章,我们要打通账号,就要能让该用户在 web 端可以查看且能修改他的个人资料或文章数据,比如下面是 users 集合里的一条记录:


{

  _openid:"oUL-m5FuRmuVmxvbYOGnXbnEDsn8",

  userId:"lidongbbsky",

  userInfo:{

    name:"李东bbsky",

    title:"杂役"

  },

  posts:[{

    title:"为什么说云开发值得普及?",

    content:"<h3>学习门槛特别低</h3><p>可以说云开发是最容易上手且最容易出成果的编程方向了</p>"

  }]

}

当我们把该集合设置为所有人可读,仅创建者可读写时,用户在小程序端对属于自己的记录可读可写,但是当该用户在 web 端时,他只能读不能写,除非使用云函数,先在数据库里查询到该用户的 openid(如果你把 userId 设计成动态刷新的话),再进行数据库和存储的增删改查,也就是用户对数据库和云存储的所有操作都需要经过云函数都需要先查询用户在小程序端的 openid,功能虽然可以实现,但是对 web 端并不是很友好,一是多了一次查询,二是不能在 web 端直接进行写操作。

4、安全规则之 openid 与 uid

如果想要不需要借助于云函数的情况下,让 web 端的用户能够更加方便的和小程序端的用户权限打通,则需要借助于安全规则,比如仅创建者可读写的安全规则是:


{

  "read": "auth.openid == doc._openid",

  "write": "auth.openid == doc._openid"

}

这个安全规则让小程序用户的 openid 与记录的_openid 字段的值相同时,就有了读写权限。也就是auth.openid 是小程序用户免登录之后的 openid。那如何让 web 端用户也有一样的权限呢?我们可以给 user 的每一个记录都新增一个 webuid 的字段,用来记录 web 端用户的 openid(uid)以及一个 wxuid 的字段,用来记录小程序端的 openid。让权限互通,这里会有四种情况:

  • 如果记录 A是用户在小程序端创建的,那这条记录自动添加的_openid 为小程序的 openid,只要 read、write 的安全规则为auth.openid == doc._openid,那小程序用户对这条记录有读写权限;

  • 如果该用户想在 web 端对记录 A有读写权限,那我们可以让 read、write 的安全规则为auth.uid == doc.webuid,这样 webd 端用户就能对记录有读写权限;

  • 如果记录 B是用户在 web 创建的,那这条记录自动添加的_openid 为 web 端用户的 openid(uid),只要 read、write 的安全规则为auth.uid == doc._openid,那 web 端用户对这条记录有读写权限;

  • 如果该用户想在小程序端对记录 B有读写权限,那我们可以让 read、write 的安全规则为auth.openid == doc.wxuid,这样 webd 端用户就能对记录有读写权限;

所以,我们可以将安全规则设置为如下,无论记录是在小程序端创建还是 web 端创建,用户都拥有跨端的可读写权限:


{

  "read": "auth.openid == doc._openid || auth.uid == doc.webuid || auth.uid == doc._openid || auth.openid == doc.wxuid",

  "write": "auth.openid == doc._openid || auth.uid == doc.webuid || auth.uid == doc._openid || auth.openid == doc.wxuid",

}

之所以这么复杂,是因为 web 端创建记录时的_openid 是用户的 uid,小程序端创建记录时的_openid 是微信生态的_openid,而要做到两套体系容易,则需要一个字段来做过渡,我们也可以只用一个字段,比如只用一个 uid 的字段,当记录_openid 是小程序的_openid 时,uid 就记录 web 端用户的 uid;当记录_openid 是 web 端用户的 uid 时,uid 就记录该用户在小程序的 openid,安全规则就可以写为:


{

  "read": "auth.openid == doc._openid || auth.uid == doc.uid || auth.uid == doc._openid || auth.openid == doc.uid",

  "write": "auth.openid == doc._openid || auth.uid == doc.uid || auth.uid == doc._openid || auth.openid == doc.uid"

}

本文出自 李东bbsky