# 数据库安全规则

开发者工具 1.02.1911252 起支持配置

安全规则是一个让开发者可以灵活地自定义前端数据库读写权限的能力,通过配置安全规则,开发者可以精细化的控制集合中所有记录的读、写权限,自动拒绝不符合安全规则的前端数据库请求,保障数据安全。使用安全规则,你将获得以下能力:

  1. 灵活自定义集合记录的读写权限:获得比基础的四种基础权限设置更灵活、强大的读写权限控制,让读写权限控制不再强制依赖于 _openid 字段和用户 openid
  2. 防止越权访问和越权更新:用户只能获取通过安全规则限制的用户所能获取的内容,越权获取数据将被拒绝
  3. 限制新建数据的内容:让新建数据必须符合规则,如可以要求权限标记字段必须为用户 openid

同时随着安全规则的开放,前端批量更新(where.updatewhere.remove)也随之开放(基础库 2.9.4 起),开发者在进行批量更新时应搭配安全规则使用以保障数据安全。

# 简介

安全规则要求前端发起的查询条件必须是安全规则的子集,否则拒绝访问。比如定义一个读写访问规则是 auth.openid == doc._openid,则表示访问时的查询条件(doc)的 openid 必须等于当前用户的 openid (由系统赋值的不可篡改的 auth.openid 给出),如果查询条件没有包含这项,则表示尝试越权访问 _openid 字段不等于自身的记录,会被后台拒绝访问。

# 与基础权限配置的对比

除了安全规则外,云开发数据库提供了四种基础权限配置,适用于简单的前端访问控制,只支持 4 种预设的规则(对集合中的每条数据记录):

  1. 所有用户可读,仅创建者可写
  2. 仅创建者可读写
  3. 所有用户可读
  4. 所有用户不可读写

但基础的设置给前端的访问权限控制是有一定局限性、同时会带来一些容易疑惑的、需要深入理解的系统默认行为:

  1. 访问权限控制要求只能基于记录的 _openid 字段和用户的 openid,控制粒度较粗、相对不灵活
  2. 当权限为 "仅创建者可读写" 时,查询时会默认给查询条件加上一条 _openid 必须等于用户 openid
  3. 当权限为 "仅创建者可读写" 或 "所有用户可读,仅创建者可写" 时,更新前会默认先带上 _openid 必须等于用户 openid 的查询条件,再将查询到的结果进行更新,即使是用 doc.update 也是如此(因此我们会见到即使我们没有对应 _id 的记录的访问权限,但是更新操作不会失败,只会在返回的结果中说明 updated 更新的记录数量为 0)。
  4. 创建记录时,会自动给记录加上 _openid 字段,值等于用户 openid,并且不允许用户在创建记录时尝试设置 _openid
  5. 更新记录时,不允许修改 _openid

因此,我们建议开发者使用新推出的数据库安全规则取代基础权限配置,可以让数据库访问的行为更加明确,同时取消需要深入理解的系统默认行为,让数据库权限控制更加简单明确。

新的安全规则与旧的四种基础权限配置的对应关系如下,我们在此先给出样例,下方再给出具体的安全规则使用指南:

新自定义安全规则与旧权限配置的对应关系

所有用户可读,仅创建者可写

{
  "read": true,
  "write": "doc._openid == auth.openid"
}

仅创建者可读写

{
  "read": "doc._openid == auth.openid",
  "write": "doc._openid == auth.openid"
}

所有用户可读

{
  "read": true,
  "write": false
}

所有用户不可读写

{
  "read": false,
  "write": false
}

# 规则编写

我们可以在控制台对各个集合分别配置安全规则,入口在集合权限配置页,在基础的四种权限配置外还提供了 “自定义规则” 的选项。

每个集合都有独立的安全规则配置,配置的格式为 json,比如如下一个在某集合上的安全规则配置:

{
  "read": "true",
  "write": "auth.openid === doc._openid"
}

这配置其实就对应着已有的 "所有用户可读,仅创建者可写" 这一权限配置。配置的 key 表示操作类型,value 是一个表达式,表示需要满足什么条件才允许相应的操作类型。当表达式解析为 true 时即代表相应类型的操作符合安全规则。

# 操作类型

支持配置的操作类型如下:

操作类型 说明 默认值
read false
write 写,可以细分为 create、update、delete false
create 新建
update 更新
delete 删除

# 规则表达式

规则表达式是类 js 的表达式,支持部分表达式,内置全局变量、全局函数。

# 全局变量

变量 类型 说明
auth object 用户登录信息,auth.openid 是用户 openid
doc object 记录内容,用于匹配记录内容/查询条件
now number 当前时间的时间戳

# 运算符

运算符 说明 示例 示例解释(集合查询)
== 等于 auth.openid == 'zzz' 用户的 openid 为 zzz
!= 不等于 auth.openid != 'zzz' 用户的 openid 不为 zzz
> 大于 doc.age>10 查询条件的 age 属性大于 10
>= 大于等于 doc.age>=10 查询条件的 age 属性大于等于 10
< 小于 doc.age<10 查询条件的 age 属性小于 10
<= 小于等于 doc.age<=10 查询条件的 age 属性小于等于 10
in 存在在集合中 auth.openid in ['zzz','aaa'] 用户的 openid 是['zzz','aaa']中的一个
!(xx in []) 不存在在集合中,使用 in 的方式描述 !(a in [1,2,3]) !(auth.openid in ['zzz','aaa']) 用户的 openid 不是['zzz','aaa']中的任何一个
&& auth.openid == 'zzz' && doc.age>10 用户的 openid 为 zzz 并且查询条件的 age 属性大于 10
|| auth.openid == 'zzz' || doc.age>10 用户的 openid 为 zzz 或者查询条件的 age 属性大于 10
. 对象元素访问符 auth.openid 用户的 openid
[] 数组访问符属性 doc.favorites[0] == 'zzz' 查询条件的 favorites 数组字段的第一项的值等于 zzz

# 全局函数

get:获取指定记录

get 函数,用于在安全规则中获取其记录来参与到安全规则的匹配中,函数的参数格式是 `database.集合名 . 记录id`,可以接收变量,值可以通过多种计算方式得到,例如使用字符串模版进行拼接(database.${doc.collction}.${doc.\_id})。

如果有对应对象,则函数返回记录的内容,否则返回空。

示例:

{
  "read": "true",
  "delete": "get(`database.user.${id}`).isManager"
}

get 函数有以下限制条件:

  1. get 参数中存在的变量 doc 需要在 query 条件中以 == 或 in 方式出现,若以 in 方式出现,只允许 in 唯一值, 即 doc.shopId in array, array.length == 1
  2. 一个表达式最多可以有 3get 函数,最多可以访问 3 个不同的文档
  3. get 函数的嵌套深度最多为 2, 即 get(get(path))

读操作触发与配额消耗说明:

get 函数的执行会计入数据库请求数,同样受数据库配额限制。在未使用变量的情况下,每个 get 会产生一次读操作,在使用变量时,对每个变量值会产生一次 get 读操作。例如:

假设某集合 shop 上有如下规则:

{
  "read": "auth.openid == get(`database.shop.${doc._id}`).owner",
  "write": false
}

在执行如下查询语句时会产生 5 次读取。

db.collection('shop').where(_.or([{_id:1},{_id:2},{_id:3},{_id:4},{_id:5}])).get()

# 规则匹配

对于查询或更新操作,输入的查询条件必须是安全规则的子集。系统不会去实际取数据,而会判断输入的查询条件是否是安全规则的子集,如果不是,则代表正在尝试访问没有权限访问的数据,会直接拒绝操作。

可能生效的操作类型包括 readwriteupdatedelete

示例:

// 集合 test 的安全规则配置限制只能查询 age > 10 的记录
{
  "read": "doc.age > 10"
}

// 符合安全规则
const res = await db.collection('test').where({
  age: _.gt(10)
}).get()

// 不符合安全规则
const res = db.collection('test').where({
  age: _.gt(8)
}).get()

对于 create,则会校验写入的数据是否符合安全规则。

# {openid} 变量

在查询时,当前用户 openid 是常用的变量,在新的安全规则体系下,要求显式传入 openid,因此为了方便开发者、让开发者无需每次先通过云函数获取用户 openid,我们规定查询条件中可使用一个字符串常量 {openid},在后台中发现该字符串时会自动替换为小程序用户的 openid,如假设有安全规则:

{
  "read": "doc.publisher == auth.openid"
}

则发起读请求时可以使用 {openid} 常量,效果等同于显示传入当前用户的实际 openid

db.collection('test').where({
  publisher: '{openid}'
}).get()

# 未登录模式

未登录模式即无登录态的模式,在未登录模式中,auth 为空,开发者可以以此判断是未登录用户的访问。未登录模式的场景有如:

  1. 单页模式:小程序/小游戏分享到朋友圈被打开时
  2. Web 未登录模式:没有登录的 Web 环境中(见多端支持

# 升级与兼容指引

由于安全规则要求查询条件是安全规则的子集,同时摒弃了旧有权限配置的隐式默认行为,因此启动安全规则需要开发者注意以下升级/兼容处理:

1. doc 操作需转为 where 操作

doc 操作(doc.get, doc.set 等)是仅指定 _id 进行的操作,因此其查询条件大部分情况下并不会满足安全规则(除非在 "read": true 下进行读操作或在 "write": true 的情况下进行写操作),因此需要转换为等价的、查询条件包含安全规则或是其子集的形式。例:

假设在集合 todo 上有以下权限规则:

{
  "read": "doc._openid == auth.openid",
  "write": "doc._openid == auth.openid"
}

旧权限配置可以通过 db.collection('todo').doc('x').get() 获取记录内容,新安全规则需要改为:

db.collection('todo').where({
  _id: 'x',
  _openid: '{openid}',
})

doc.update, doc.remove 同理,注意 doc.set 无法使用,需要用 doc.update 替代。

2. 从旧权限配置升级后,查询更新语句都需明确指定 openid

因升级前查询条件可以不传 _openid,而升级后要求显示传入以保证查询条件符合安全规则,因此所有查询条件均需传入 openid,还是以上一节中的安全规则示例为例,对旧权限配置中的如下查询语句:

db.collection('todo').where({
  progress: _.lt(50)
}).get()

需要改为:

db.collection('todo').where({
  _openid: '{openid}',
  progress: _.lt(50)
}).get()

在开放安全规则后,where.updatewhere.remove 也在小程序端开放了,可以进行符合安全规则的批量更新,如:

db.collection('todo').where({
  _openid: '{openid}',
  category: 'sport'
}).update({
  progress: _.inc(10)
})

# 示例

以下给出三个简易示例:群聊、信息流评论、商品订单管理。

# 示例 1:群聊

# 集合定义

user

{
  _id: string,
  _openid: string,
  name: string,
}

room

{
  _id: string,
  owner: string, // 群主 openid
  name: string, // 群名
  members: string[], // 成员 openid 列表
}

message

{
  _id: string,
  room: string, // 房间 id
  sender: string, // 发送者 openid
  content: string, // 消息内容
  time: Date, // 发送时间
  withdrawn: boolean, // 是否已撤回
}

# 权限规则

user 权限规则

{
  "read": "doc._openid == auth.openid", // 私有读
  "write": "doc._openid == auth.openid", // 仅能修改自己的信息
}

room 权限规则

{
  "read": "auth.openid in get('database.room.${doc._id}').members", // 仅群成员可以读群信息
  // 要求管理房间的写操作不能在前端:
  //  - 原子:建群时需保证 room 集合的 members 和各个成员的 rooms 都写入
  //  - 权限:仅群主能修改群信息
  //  - 权限:仅群成员可以拉新成员进群
  //  - 权限:仅群主可以踢人
  "write": false
}

message 权限规则

{
  // 仅能读取自己所在房间的聊天消息,且不允许读取已撤回的消息
  "read": "auth.openid in get('database.room.${doc.room}').members && doc.withdrawn == false",
  // 只能在云函数写:
  //  - 仅能在自己所在的房间发消息
  //  - 只能修改自己发送的消息
  //  - 不能删除自己发送的消息(只能撤回)
  "create": "auth.openid in get('database.room.${doc.room}').members",
  "update": "auth.openid == doc.sender",
  "delete": false
}

# 查询 / 监听示例

监听自己所在的某个房间的某个时间点之后的新消息(就是监听已接收的某个消息后的新消息):

wx.cloud.init({
  env: '环境 ID',
})
const db = wx.cloud.database()
const _ = db.command

const watcher = db.collection('message').where({
  room: '房间 id',
  time: _.gt(new Date('2019-09-01 10:00')),
}).watch({
  onChange: snapshot => {
    console.log(`新事件`, snapshot)
  },
  onError: err => {
    console.error(`监听错误`, err)
  }
})

# 示例 2:信息流评论

# 集合定义

user: 用户信息集合,以用户 openidid

{
  _id: string, // openid
  _openid: string,
  name: string,
  isManager: boolean, // 管理员标记位
}

article: 文章集合

{
  _id: string,
  publisher: string, // 发布者 openid
  content: string, // 内容
}

comment: 评论集合

{
  _id: string,
  commenter: string, // 评论者 openid
  articleId: string, // 被评论的文章 id
  content: string, // 评论内容
}

# 安全规则

article 安全规则

{
  "read": true, // 公有读
  "create": "doc.publisher == auth.openid", // 都可以发文章,但对数据一致性校验,要求 publisher 为发布者 openid
  "update": "doc.publisher == auth.openid || get('database.user.${auth.openid}').isManager", // 仅发布者或管理员可以更新
  "delete": "doc.publisher == auth.openid || get('database.user.${auth.openid}').isManager", // 仅发布者或管理员可以删除
}

comment 安全规则

{
  "read": true, // 公有读
  "create": "doc.commenter == auth.openid", // 都可以发评论,但对数据一致性校验,要求 publisher 为发布者 openid
  "update": "doc.commenter == auth.openid || get('database.user.${auth.openid}').isManager", // 仅发布者或管理员可以更新
  "delete": "doc.commenter == auth.openid || get('database.user.${auth.openid}').isManager", // 仅发布者或管理员可以删除
}

# 查询示例

创建一条评论:

wx.cloud.init({
  env: '环境 ID',
})
const db = wx.cloud.database()
const _ = db.command

const result = await db.collection('comment').add({
  data: {
    commenter: '{openid}', // 用 {openid} 变量,后台会自动替换为当前用户 openid
    articleId: '文章 ID',
    content: '评论内容',
  },
})

console.log('创建结果', result)

# 示例 3:商品订单管理

假设需要构建一个简易商品管理系统,有商店信息,一个商店对应多个商品、多个订单,商品信息公开可查,只有商店所有者或管理员可以查看自己商店的订单信息。

# 集合定义

shop 商店集合

{
  _id: string,
  name: string, // 商店名
  location: GeoPoint, // 商店地理位置
  owner: string, // 商店拥有者 openid
  managers: string[], // 商店管理员 openid 列表
}

item 商品集合

{
  _id: string,
  shopId: string, // 所在商店
  name: string, // 商品名
  price: number, // 价格
  stock: number, // 库存
}

order 订单集合

{
  _id: string,
  shopId: string, // 下单的商店
  itemId: string, // 下单的商品
  price: number, // 成交价格
  amount: number, // 成交数量
  status: string, // 状态
  createTime: Date, // 创建时间
  updateTime: Date, // 更新时间
}

# 权限规则

shop 安全规则:

{
  "read": true, // 公有读
  "write": false, // 仅云函数端写
}

item 安全规则:

{
  // 公有读
  "read": true,
  // 仅商店所有者或管理员可写
  "write": "auth.openid == get(`database.shop.${doc.shopId}`).owner || auth.openid in get(`database.shop.${doc.shopId}`).managers",
}

order 安全规则:

{
  // 仅商店所有者或管理员可读写
  "read": "auth.openid == get(`database.shop.${doc.shopId}`).owner || auth.openid in get(`database.shop.${doc.shopId}`).managers",
  "write": "auth.openid == get(`database.shop.${doc.shopId}`).owner || auth.openid in get(`database.shop.${doc.shopId}`).managers",
  // 仅云函数端可删除订单记录
  "delete": false,
}

# 查询 / 监听示例

监听自己所在商店的新订单动态:

wx.cloud.init({
  env: '环境 ID',
})
const db = wx.cloud.database()
const _ = db.command

const watcher = db.collection('order').where({
  shopId: '商店 id',
  createTime: _.gt(new Date()),
}).watch({
  onChange: snapshot => {
    console.log(`新事件`, snapshot)
  },
  onError: err => {
    console.error(`监听错误`, err)
  }
})