權限控制

2020-07-20 10:32 更新

數據庫的權限分為小程序端和管理端,管理端包括云函數端和控制臺。小程序端運行在小程序中,讀寫數據庫受權限控制限制,管理端運行在云函數上,擁有所有讀寫數據庫的權限。云控制臺的權限同管理端,擁有所有權限。小程序端操作數據庫應有嚴格的安全規(guī)則限制。

我們提供了兩種權限控制方案,第一種是初期提供的基礎的四種簡易權限設置,第二種是靈活的、可自定義的權限控制,即數據庫安全規(guī)則。

基礎權限設置

以下幾種權限配置是基礎的簡易權限配置,如果需要靈活的自定義權限配置,請使用數據庫安全規(guī)則

每個集合可以擁有一種權限配置,權限配置的規(guī)則是作用在集合的每個記錄上的。出于易用性和安全性的考慮,云開發(fā)為云數據庫做了小程序深度整合,在小程序中創(chuàng)建的每個數據庫記錄都會帶有該記錄創(chuàng)建者(即小程序用戶)的信息,以 _openid 字段保存用戶的 openid 在每個相應用戶創(chuàng)建的記錄中。因此,權限控制也相應圍繞著一個用戶是否應該擁有權限操作其他用戶創(chuàng)建的數據展開。

以下按照權限級別從寬到緊排列如下:

  • 僅創(chuàng)建者可寫,所有人可讀:數據只有創(chuàng)建者可寫、所有人可讀;比如文章。
  • 僅創(chuàng)建者可讀寫:數據只有創(chuàng)建者可讀寫,其他用戶不可讀寫;比如用私密相冊。
  • 僅管理端可寫,所有人可讀:該數據只有管理端可寫,所有人可讀;如商品信息。
  • 僅管理端可讀寫:該數據只有管理端可讀寫;如后臺用的不暴露的數據。

簡而言之,管理端始終擁有讀寫所有數據的權限,小程序端始終不能寫他人創(chuàng)建的數據,小程序端的記錄的讀寫權限其實分為了 “所有人可讀,只有創(chuàng)建者可寫“、”僅創(chuàng)建者可讀寫“、”所有人可讀,僅管理端可寫“、”所有人不可讀,僅管理端可讀寫“。

對一個用戶來說,不同模式在小程序端和管理端的權限表現(xiàn)如下:

模式小程序端
讀自己創(chuàng)建的數據
小程序端
寫自己創(chuàng)建的數據
小程序端
讀他人創(chuàng)建的數據
小程序端
寫他人創(chuàng)建的數據
管理端
讀寫任意數據
僅創(chuàng)建者可寫,所有人可讀×
僅創(chuàng)建者可讀寫××
僅管理端可寫,所有人可讀××
僅管理端可讀寫:該數據只有管理端可讀寫××××

在設置集合權限時應謹慎設置,防止出現(xiàn)越權操作。

數據庫安全規(guī)則

開發(fā)者工具 1.02.1911252 起支持配置

安全規(guī)則是一個讓開發(fā)者可以靈活地自定義前端數據庫讀寫權限的能力,通過配置安全規(guī)則,開發(fā)者可以精細化的控制集合中所有記錄的讀、寫權限,自動拒絕不符合安全規(guī)則的前端數據庫請求,保障數據安全。使用安全規(guī)則,你將獲得以下能力:

  1. 靈活自定義集合記錄的讀寫權限:獲得比基礎的四種基礎權限設置更靈活、強大的讀寫權限控制,讓讀寫權限控制不再強制依賴于 _openid 字段和用戶 openid
  2. 防止越權訪問和越權更新:用戶只能獲取通過安全規(guī)則限制的用戶所能獲取的內容,越權獲取數據將被拒絕
  3. 限制新建數據的內容:讓新建數據必須符合規(guī)則,如可以要求權限標記字段必須為用戶 openid

同時隨著安全規(guī)則的開放,前端批量更新(where.update、where.remove)也隨之開放(基礎庫 2.9.4 起),開發(fā)者在進行批量更新時應搭配安全規(guī)則使用以保障數據安全。

簡介

安全規(guī)則要求前端發(fā)起的查詢條件必須是安全規(guī)則的子集,否則拒絕訪問。比如定義一個讀寫訪問規(guī)則是 auth.openid == doc._openid,則表示訪問時的查詢條件(doc)的 openid 必須等于當前用戶的 openid (由系統(tǒng)賦值的不可篡改的 auth.openid 給出),如果查詢條件沒有包含這項,則表示嘗試越權訪問 _openid 字段不等于自身的記錄,會被后臺拒絕訪問。

與基礎權限配置的對比

除了安全規(guī)則外,云開發(fā)數據庫提供了四種基礎權限配置,適用于簡單的前端訪問控制,只支持 4 種預設的規(guī)則(對集合中的每條數據記錄):

  1. 所有用戶可讀,僅創(chuàng)建者可寫
  2. 僅創(chuàng)建者可讀寫
  3. 所有用戶可讀
  4. 所有用戶不可讀寫

但基礎的設置給前端的訪問權限控制是有一定局限性、同時會帶來一些容易疑惑的、需要深入理解的系統(tǒng)默認行為:

  1. 訪問權限控制要求只能基于記錄的 _openid 字段和用戶的 openid,控制粒度較粗、相對不靈活
  2. 當權限為 "僅創(chuàng)建者可讀寫" 時,查詢時會默認給查詢條件加上一條 _openid 必須等于用戶 openid
  3. 當權限為 "僅創(chuàng)建者可讀寫" 或 "所有用戶可讀,僅創(chuàng)建者可寫" 時,更新前會默認先帶上 _openid 必須等于用戶 openid 的查詢條件,再將查詢到的結果進行更新,即使是用 doc.update 也是如此(因此我們會見到即使我們沒有對應 _id 的記錄的訪問權限,但是更新操作不會失敗,只會在返回的結果中說明 updated 更新的記錄數量為 0)。
  4. 創(chuàng)建記錄時,會自動給記錄加上 _openid 字段,值等于用戶 openid,并且不允許用戶在創(chuàng)建記錄時嘗試設置 _openid
  5. 更新記錄時,不允許修改 _openid

因此,我們建議開發(fā)者使用新推出的數據庫安全規(guī)則取代基礎權限配置,可以讓數據庫訪問的行為更加明確,同時取消需要深入理解的系統(tǒng)默認行為,讓數據庫權限控制更加簡單明確。

新的安全規(guī)則與舊的四種基礎權限配置的對應關系如下,我們在此先給出樣例,下方再給出具體的安全規(guī)則使用指南:

新自定義安全規(guī)則與舊權限配置的對應關系

所有用戶可讀,僅創(chuàng)建者可寫

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

僅創(chuàng)建者可讀寫

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

所有用戶可讀

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

所有用戶不可讀寫

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

規(guī)則編寫

我們可以在控制臺對各個集合分別配置安全規(guī)則,入口在集合權限配置頁,在基礎的四種權限配置外還提供了 “自定義規(guī)則” 的選項。

每個集合都有獨立的安全規(guī)則配置,配置的格式為 json,比如如下一個在某集合上的安全規(guī)則配置:

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

這配置其實就對應著已有的 "所有用戶可讀,僅創(chuàng)建者可寫" 這一權限配置。配置的 key 表示操作類型,value 是一個表達式,表示需要滿足什么條件才允許相應的操作類型。當表達式解析為 true 時即代表相應類型的操作符合安全規(guī)則。

操作類型

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

操作類型說明默認值
readfalse
write寫,可以細分為 create、update、deletefalse
create新建
update更新
delete刪除

規(guī)則表達式

規(guī)則表達式是類 js 的表達式,支持部分表達式,內置全局變量、全局函數。

全局變量

變量類型說明
authobject用戶登錄信息,auth.openid 是用戶 openid
docobject記錄內容,用于匹配記錄內容/查詢條件
nownumber當前時間的時間戳

運算符

運算符說明示例示例解釋(集合查詢)
==等于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 函數,用于在安全規(guī)則中獲取其記錄來參與到安全規(guī)則的匹配中,函數的參數格式是 `database.集合名.記錄id`,可以接收變量,值可以通過多種計算方式得到,例如使用字符串模版進行拼接(database.${doc.collction}.${doc.\_id})。

如果有對應對象,則函數返回記錄的內容,否則返回空。

示例:

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

get 函數有以下限制條件:

  1. get 參數中存在的變量 doc 需要在 query 條件中以 == 或 in 方式出現(xiàn),若以 in 方式出現(xiàn),只允許 in 唯一值, 即 doc.shopId in array, array.length == 1
  2. 一個表達式最多可以有 3 個 get 函數,最多可以訪問 3 個不同的文檔。
  3. get 函數的嵌套深度最多為 2, 即 get(get(path))。

讀操作觸發(fā)與配額消耗說明:

get 函數的執(zhí)行會計入數據庫請求數,同樣受數據庫配額限制。在未使用變量的情況下,每個 get 會產生一次讀操作,在使用變量時,對每個變量值會產生一次 get 讀操作。例如:

假設某集合 shop 上有如下規(guī)則:

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

在執(zhí)行如下查詢語句時會產生 5 次讀取。

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

規(guī)則匹配

對于查詢或更新操作,輸入的查詢條件必須是安全規(guī)則的子集。系統(tǒng)不會去實際取數據,而會判斷輸入的查詢條件是否是安全規(guī)則的子集,如果不是,則代表正在嘗試訪問沒有權限訪問的數據,會直接拒絕操作。

可能生效的操作類型包括 read、write、update、delete

示例:

// 集合 test 的安全規(guī)則配置限制只能查詢 age > 10 的記錄
{
  "read": "doc.age > 10"
}

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

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

對于 create,則會校驗寫入的數據是否符合安全規(guī)則。

{openid} 變量

在查詢時,當前用戶 openid 是常用的變量,在新的安全規(guī)則體系下,要求顯式傳入 openid,因此為了方便開發(fā)者、讓開發(fā)者無需每次先通過云函數獲取用戶 openid,我們規(guī)定查詢條件中可使用一個字符串常量 {openid},在后臺中發(fā)現(xiàn)該字符串時會自動替換為小程序用戶的 openid,如假設有安全規(guī)則:

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

則發(fā)起讀請求時可以使用 {openid} 常量,效果等同于顯示傳入當前用戶的實際 openid :

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

未登錄模式

未登錄模式即無登錄態(tài)的模式,在未登錄模式中,auth 為空,開發(fā)者可以以此判斷是未登錄用戶的訪問。未登錄模式的場景有如:

  1. 單頁模式:小程序/小游戲分享到朋友圈被打開時
  2. Web 未登錄模式:沒有登錄的 Web 環(huán)境中(見多端支持

升級與兼容指引

由于安全規(guī)則要求查詢條件是安全規(guī)則的子集,同時摒棄了舊有權限配置的隱式默認行為,因此啟動安全規(guī)則需要開發(fā)者注意以下升級/兼容處理:

1. doc 操作需轉為 where 操作

因 doc 操作(doc.get, doc.set 等)是僅指定 _id 進行的操作,因此其查詢條件大部分情況下并不會滿足安全規(guī)則(除非在 "read": true 下進行讀操作或在 "write": true 的情況下進行寫操作),因此需要轉換為等價的、查詢條件包含安全規(guī)則或是其子集的形式。例:

假設在集合 todo 上有以下權限規(guī)則:

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

舊權限配置可以通過 db.collection('todo').doc('x').get() 獲取記錄內容,新安全規(guī)則需要改為:

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

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

2. 從舊權限配置升級后,查詢更新語句都需明確指定 openid

因升級前查詢條件可以不傳 _openid,而升級后要求顯示傳入以保證查詢條件符合安全規(guī)則,因此所有查詢條件均需傳入 openid,還是以上一節(jié)中的安全規(guī)則示例為例,對舊權限配置中的如下查詢語句:

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

需要改為:

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

在開放安全規(guī)則后,where.update 和 where.remove 也在小程序端開放了,可以進行符合安全規(guī)則的批量更新,如:

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, // 發(fā)送者 openid
  content: string, // 消息內容
  time: Date, // 發(fā)送時間
  withdrawn: boolean, // 是否已撤回
}

權限規(guī)則

user 權限規(guī)則

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

room 權限規(guī)則

{
  "read": "auth.openid in get('database.room.${doc._id}').members", // 僅群成員可以讀群信息
  // 要求管理房間的寫操作不能在前端:
  //  - 原子:建群時需保證room集合的members和各個成員的rooms都寫入
  //  - 權限:僅群主能修改群信息
  //  - 權限:僅群成員可以拉新成員進群
  //  - 權限:僅群主可以踢人
  "write": false
}

message 權限規(guī)則

{
  // 僅能讀取自己所在房間的聊天消息,且不允許讀取已撤回的消息
  "read": "auth.openid in get('database.room.${doc.room}').members && doc.withdrawn == false",
  // 只能在云函數寫:
  //  - 僅能在自己所在的房間發(fā)消息
  //  - 只能修改自己發(fā)送的消息
  //  - 不能刪除自己發(fā)送的消息(只能撤回)
  "create": "auth.openid in get('database.room.${doc.room}').members",
  "update": "auth.openid == doc.sender",
  "delete": false
}

查詢 / 監(jiān)聽示例

監(jiān)聽自己所在的某個房間的某個時間點之后的新消息(就是監(jiān)聽已接收的某個消息后的新消息):

wx.cloud.init({
  env: '環(huán)境 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(`監(jiān)聽錯誤`, err)
  }
})

示例 2:信息流評論

集合定義

user: 用戶信息集合,以用戶 openid 為 id

{
  _id: string, // openid
  _openid: string,
  name: string,
  isManager: boolean, // 管理員標記位
}

article: 文章集合

{
  _id: string,
  publisher: string, // 發(fā)布者 openid
  content: string, // 內容
}

comment: 評論集合

{
  _id: string,
  commenter: string, // 評論者 openid
  articleId: string, // 被評論的文章 id
  content: string, // 評論內容
}

安全規(guī)則

article 安全規(guī)則

{
  "read": true, // 公有讀
  "create": "doc.publisher == auth.openid", // 都可以發(fā)文章,但對數據一致性校驗,要求 publisher 為發(fā)布者 openid
  "update": "doc.publisher == auth.openid || get('database.user.${auth.openid}').isManager", // 僅發(fā)布者或管理員可以更新
  "delete": "doc.publisher == auth.openid || get('database.user.${auth.openid}').isManager", // 僅發(fā)布者或管理員可以刪除
}

comment 安全規(guī)則

{
  "read": true, // 公有讀
  "create": "doc.commenter == auth.openid", // 都可以發(fā)評論,但對數據一致性校驗,要求 publisher 為發(fā)布者 openid
  "update": "doc.commenter == auth.openid || get('database.user.${auth.openid}').isManager", // 僅發(fā)布者或管理員可以更新
  "delete": "doc.commenter == auth.openid || get('database.user.${auth.openid}').isManager", // 僅發(fā)布者或管理員可以刪除
}

查詢示例

創(chuàng)建一條評論:

wx.cloud.init({
  env: '環(huán)境 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('創(chuàng)建結果', result)

示例 3:商品訂單管理

假設需要構建一個簡易商品管理系統(tǒng),有商店信息,一個商店對應多個商品、多個訂單,商品信息公開可查,只有商店所有者或管理員可以查看自己商店的訂單信息。

集合定義

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, // 狀態(tài)
  createTime: Date, // 創(chuàng)建時間
  updateTime: Date, // 更新時間
}

權限規(guī)則

shop 安全規(guī)則:

{
  "read": true, // 公有讀
  "write": false, // 僅云函數端寫
}

item 安全規(guī)則:

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

order 安全規(guī)則:

{
  // 僅商店所有者或管理員可讀寫
  "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,
}

查詢 / 監(jiān)聽示例

監(jiān)聽自己所在商店的新訂單動態(tài):

wx.cloud.init({
  env: '環(huán)境 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(`監(jiān)聽錯誤`, err)
  }
})


以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號