14.7 自定义登录

匿名登录可以让用户在无需注册登录的情况下在短期内使用数据库、云存储以及调用云函数,但是大多数的应用是需要获取用户的身份才能更长期更安全将数据存储在云端,并且可以获取跨设备跨端的一致性的体验。在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('tcb-admin-node') 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('tcb-admin-node') 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" }