用户体系
99%的应用,都要开发用户注册、登录、发送短信验证码、修改密码、密码加密保存、密码防探测、token管理、页面访问权限、注册用户统计等众多功能,从前端到后端都需要。
uni-id应需而生。
uni-id为uniCloud开发者提供了开源、易用、安全、丰富、可扩展的用户管理框架。
# 组成结构
uni-id贯穿了uni-app前端到uniCloud后端的各个环节。
| 模块 | 说明 |
|---|---|
| 前端uni-app框架的相关API | uniIdRouter页面路由、token管理客户端API |
| 前端页面uni-id-pages | 登录、注册、修改密码、忘记密码、个人中心、修改头像等前端页面 |
| 网络传输自动管理用户token | 自动保存、续期token、网络自动传输token |
| 云端云对象uni-id-co | 与uni-id-pages搭配的云对象,相关业务的云端部分 |
| 云端配置uni-config-center | 在uni-config-center下提供各种配置 |
| 云端公共模块uni-id-common | 用于云函数或云对象集成该模块验证token身份 |
| 云数据库的用户相关数据表 | uni-id-users等各种opendb数据表 |
| uni-admin | Admin管理后台,包括用户角色权限管理、注册用户统计 |
# uni-id用户体系
uni-id的云端配置文件在uniCloud/cloudfunctions/common/uni-config-center/uni-id/config.json中。
因此我们在uniCloud/cloudfunctions/common/uni-config-center下新建uni-id文件夹,并创建config.json文件,将以下配置拷贝进去
// 如果拷贝此内容切记去除注释
{
"passwordSecret": [
{
"type": "hmac-sha256",
"version": 1
}
], // 数据库中password字段是加密存储的,这里的passwordSecret即为加密密码所用的加密算法,详见[密码安全]
"passwordStrength": "medium", // 密码强度,新增于 uni-id-pages 1.0.8版本,见下方说明
"tokenSecret": "", // 生成token所用的密钥,注意修改为自己的,使用一个较长的字符串即可
"requestAuthSecret": "", // URL化请求鉴权签名密钥
"tokenExpiresIn": 7200, // 全平台token过期时间,未指定过期时间的平台会使用此值
"tokenExpiresThreshold": 3600, // 新增于uni-id 1.1.7版本,checkToken时如果token有效期小于此值且在有效期内则自动获取新token,请注意将新token返回给前端保存(云对象会自动保存符合uniCloud响应体规范的响应内的新token),如果不配置此参数则不开启自动获取新token功能
"maxTokenLength": 10, // 数据库用户记录token数组的最大长度,默认为10。新增于uni-id-common 1.0.16
"passwordErrorLimit": 6, // 密码错误最大重试次数
"passwordErrorRetryTime": 3600, // 密码错误重试次数超限之后的冻结时间
"autoSetInviteCode": false, // 是否在用户注册时自动设置邀请码,默认不自动设置
"forceInviteCode": false, // 是否强制用户注册时必填邀请码,默认为false
"idCardCertifyLimit": 1, // 实名认证相关; 限制每个身份证可以绑定几个账号
"realNameCertifyLimit": 5, // 实名认证相关; 限制用户每日认证次数,防止接口被刷
"sensitiveInfoEncryptSecret": "", // 敏感信息加密密钥(长度为32位的字符串),如使用实名认证功能需配置此密钥
"frvNeedAlivePhoto": false, // 实名认证相关;是否获取认证照片
"userRegisterDefaultRole": [], // 用户注册时的默认角色
"app": { // 如果你使用旧版本uni-id公共模块而不是uni-id-common这里可能配置的是app-plus,务必注意调整为app
"tokenExpiresIn": 2592000,
"tokenExpiresThreshold": 864000,
"oauth": {
// App微信登录所用到的appid、appsecret需要在微信开放平台获取,注意:不是公众平台而是开放平台
"weixin": {
"appid": "",
"appsecret": ""
},
// App QQ登录所用到的appid、appsecret需要在腾讯开放平台获取,注意:不是公众平台而是开放平台
"qq": {
"appid": "",
"appsecret": ""
},
"apple": { // 使用苹果登录时需要
"bundleId": ""
},
"huawei": { // 鸿蒙应用使用华为登录
"clientId": "",
"clientSecret": ""
}
}
},
"web": { // 如果你使用旧版本uni-id公共模块而不是uni-id-common这里可能配置的是h5,务必注意调整为web
"tokenExpiresIn": 7200,
"tokenExpiresThreshold": 3600,
"oauth": {
"weixin-h5": { // 微信公众号登录配置
"appid": "",
"appsecret": ""
},
"weixin-web": { // 微信PC页面扫码登录配置
"appid": "",
"appsecret": ""
}
}
},
"mp-weixin": {
"tokenExpiresIn": 259200,
"tokenExpiresThreshold": 86400,
"oauth": {
// 微信小程序登录所用的appid、appsecret需要在对应的小程序管理控制台获取
"weixin": {
"appid": "",
"appsecret": ""
}
}
},
"mp-qq": {
"tokenExpiresIn": 259200,
"tokenExpiresThreshold": 86400,
"oauth": {
// QQ小程序登录所用的appid、appsecret需要在对应的小程序管理控制台获取
"qq": {
"appid": "",
"appsecret": ""
}
}
},
"mp-alipay": {
"tokenExpiresIn": 259200,
"tokenExpiresThreshold": 86400,
"oauth": {
// 支付宝小程序登录用到的appid、privateKey请参考支付宝小程序的文档进行设置或者获取,https://opendocs.alipay.com/open/291/105971#LDsXr
"alipay": {
"appid": "",
"privateKey": "", // 私钥
"keyType": "PKCS8" // 私钥类型,如果私钥类型不是PKCS8,需要填写此字段,否则会出现“error:0D0680A8:asn1 encoding routines:ASN1_CHECK_TLEN:wrong tag”错误
}
}
},
"mp-harmony": {
"oauth": {
"huawei": { // 鸿蒙元服务使用华为登录
"clientId": "",
"clientSecret": ""
}
}
},
"service": {
"sms": {
"name": "", // 应用名称,对应短信模版的name
"codeExpiresIn": 180, // 验证码过期时间,单位为秒,注意一定要是60的整数倍
"scene": {
"bind-mobile-by-sms": { // 对绑定手机号场景的配置,短信验证码场景值参考:https://uniapp.dcloud.net.cn/uniCloud/uni-id/summary.html#sms-scene
"templateId": "", // 绑定手机号使用的短信验证码模板
"codeExpiresIn": 240 // 绑定手机号验证码过期时间
}
}
},
"univerify": {
"appid": "" // uni-id-co 1.1.17及以上版本无需填写,当前应用的appid,使用云函数URL化,此项必须配置
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# 配置 tokenSecret和 requestAuthSecret
- "tokenSecret":"xxm123" 生成token所用的密钥,注意修改为自己的,使用一个较长的字符串即可
- "requestAuthSecret":"xxm123" URL化请求鉴权签名密钥
如果已经开始使用的话不要改,改了之前的用户就登录不上了
# uni-id-pages
开发者在项目中引入uni-id-pages,账户管理的功能无需自己再开发。由于源码的开放性和层次结构清晰,有二次开发需求也很方便调整。
下载安装uni-id-pages
安装成功了会看到pages.json下添加了很多页面,我们上传所有database,如果不上传会有问题, 打开其中一页来看一下

此时,点注册应该可以正常注册一个账号了,然后可以用这个账号登录
可以看到这个就是刚刚我注册的用户
# tokenExpiresIn保持客户端登录状态和uniIdRouter配置
打开个人资料页面

# 设置token有效时间
config.json中的tokenExpiresIn为token过期时间,单位为毫秒
一般来说token的有效期不会无限长,示例配置内web端token有效期为2小时,微信小程序为3天,app端为30天。你可以回忆一下你所用的软件,只要每天都打开就一直不需要重新登录,这样就牵扯到保持客户端的登录状态的问题。
uni-id使用了判断token剩余有效时间小于一定的阈值(配置文件内的tokenExpiresThreshold)但是大于0时自动下发新token的逻辑来保证活跃客户端一直处于登录状态,返回新token的逻辑由checkToken方法实现。具体该将token有效期和token刷新阈值设置为多少,需要根据多数用户软件使用频率来确定。
# 配置token过期自动跳转
在pages.json中有个配置是uniIdEouter
uniIdRouter 是一个运行在前端的、对前端页面访问权限路由进行控制的方案。
大多数应用,都会指定某些页面需要登录才能访问。以往开发者需要写不少代码。
现在,只需在项目的pages.json内配置登录页路径、需要登录才能访问的页面等信息,uni-app框架的路由跳转,会自动在需要登录且客户端登录状态过期或未登录时跳转到登录页面。
结合以下代码及注释了解如何使用uniIdRouter
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "uni-app"
},
"needLogin": false // 当前页面是否需要登录才可以访问,此配置优先级高于uniIdRouter下的needLogin
}, {
"path": "pages/list/list",
"style": {
"navigationBarTitleText": "uni-app"
},
"needLogin": true
}, {
"path": "pages/detail/detail",
"style": {
"navigationBarTitleText": "uni-app"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
"uniIdRouter": {
"loginPage": "pages/index/index", // 登录页面路径
"needLogin": [
"pages/detail/.*" // 需要登录才可访问的页面列表,可以使用正则语法
],
"resToLogin": true // 自动解析云对象及clientDB的错误码,如果是客户端token不正确或token过期则自动跳转配置的登录页面,配置为false则关闭此行为,默认true
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 通过条件编译区别设备微信快捷登录和账号密码登录
- 设置小程序的appID
- 在config.json中设置 mp-weixin.oauth.appid以及mp-weixin.oauth.appsecret,这个appsecret需要在微信的控制台里重置,重置一次后自己保存好密钥不要弄丢,再重置其他应用就会失效了
在pages.json中添加条件编译
//....
"uniIdRouter": {
// #ifdef MP-WEIXIN
"loginPage": "uni_modules/uni-id-pages/pages/loginWx",
// #endif
// #ifndef MP-WEIXIN
"loginPage": "uni_modules/uni-id-pages/pages/login"
// #endif
}
//...
2
3
4
5
6
7
8
9
10
11
# 开通sms短信服务login-by-sms短信验证码登录
到服务空间的短信服务中去


然后去签名配置中添加签名,现在需要是企业才可以
如果签名通过了就去配置模板
然后去配置config.json

# 按需展示登录方式和用户服务隐私政策
在图中的config.js中配置

# 借助内置openDB表创建并新增文章数据
创建一个新的schema

这个表相当复杂,我们删减一点东西
{
"bsonType": "object",
"required": [
],
"permission": {
"read": true,
"create": true,
"update": true,
"delete": true
},
"properties": {
"_id": {
"description": "存储文档 ID(用户 ID),系统自动生成"
},
"user_id": {
"bsonType": "string",
"description": "文章作者ID, 参考`uni-id-users` 表",
"foreignKey": "uni-id-users._id",
"defaultValue": {
"$env": "uid"
}
},
"title": {
"bsonType": "string",
"title": "标题",
"description": "标题",
"label": "标题",
"trim": "both"
},
"content": {
"bsonType": "string",
"title": "文章内容",
"description": "文章内容",
"label": "文章内容",
"trim": "right"
},
"createTime" :{
"bsonType": "timestamp",
"description": "发布时间",
"defaultValue": {
"$env": "now"
}
}
},
"version": "0.0.1"
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
然后新建一个页面,名字叫articles
<template>
<view>
</view>
</template>
<script setup>
const db = uniCloud.database()
db.collection("demo-articles").add({
title: "标题1",
content: "内容1"
}).then(res => {
console.log(res)
})
</script>
<style>
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
我们登录完刷新这个文章页面,会发现数据库中添加了一条文章

# 联表查询文章和uni-id-user表的权限校验
新建list页面
只要schema中的bsonType是password,就是加密字段,不允许被查询

因此查询uni-id-users表需要用field筛选出具体字段
<template>
<view class="">
<view class="item" v-for="(item,index) in listData" :key="item._id">
<view>标题:{{item.title}}</view>
<view>内容:{{item.content}}</view>
<view>发布者:{{item?.user_id[0].nickname || "未命名"}}</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
const db = uniCloud.database()
const listData = ref([])
const getData = async () => {
let artTemp = db.collection("demo-articles").getTemp()
let userTemp = db.collection("uni-id-users").field("nickname, _id").getTemp()
let res = await db.collection(artTemp, userTemp).get()
listData.value = res.result.data
console.log(res)
}
getData()
</script>
<style lang="scss" scoped>
.item {
padding: 30rpx;
border-bottom: 1px solid #ccc;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# Schema表permission权限控制
DB Schema的数据权限系统,是为JQL设计的,尤其clientDB强依赖这套权限系统。因为客户端是无法信任的,没有缜密的权限系统,会导致客户端任意改动云数据库内容。
DB Schema的permission规则,分为两部分,一边是对操作数据的指定,一边是对角色的指定,规则中对两者进行关联,匹配则校验通过。
# 权限规则内可用的全局变量
| 变量名 | 说明 |
|---|---|
| auth.uid | 用户id |
| auth.role | 用户角色数组,参考uni-id 角色权限,注意admin为内置的角色,如果用户角色列表里包含admin则认为此用户有完全数据访问权限 |
| auth.permission | 用户权限数组,参考uni-id 角色权限 |
| doc | 数据库中的目标数据记录,用于匹配记录内容/查询条件 |
| now | 当前服务器时间戳(单位:毫秒),时间戳可以进行额外运算,如doc.publish_date > now - 60000表示publish_date在最近一分钟 |
//...
"permission": {
"read": "auth.uid != null", //必须登录才可以读到
"create": true,
"update": true,
"delete": true
},
//...
2
3
4
5
6
7
8
# jql语句内云端环境变量
| 参数名 | 说明 |
|---|---|
| $cloudEnv_uid | 用户uid,依赖uni-id |
| $cloudEnv_now | 服务器时间戳 |
| $cloudEnv_clientIP | 当前客户端IP |
//...
"permission": {
"read": "auth.uid != doc.user_id", //只能查当前登录用户的
"create": true,
"update": true,
"delete": true
},
//...
2
3
4
5
6
7
8
// 同时查的时候也要用where做筛选
let artTemp = db.collection("demo-articles").where(`user_id == $cloudEnv_uid`).getTemp()
let userTemp = db.collection("uni-id-users").field("nickname, _id").getTemp()
let res = await db.collection(artTemp, userTemp).get()
2
3
4
# 获取当前用户信息
uniCloud.getCurrentUserInfo()
| 字段 | 类型 | 说明 |
|---|---|---|
| uid | Number | 当前用户uid |
| role | Array | 用户角色列表。admin用户返回["admin"] |
| permission | Array | 用户权限列表。注意admin角色此数组为空 |
| tokenExpired | Number | token过期时间 |