安全規(guī)則是一個(gè)可以靈活地自定義數(shù)據(jù)庫(kù)和云存儲(chǔ)讀寫權(quán)限的權(quán)限控制方式,通過(guò)配置安全規(guī)則,開發(fā)者可以在小程序端、網(wǎng)頁(yè)端精細(xì)化的控制云存儲(chǔ)和集合中所有記錄的增、刪、改、查權(quán)限,自動(dòng)拒絕不符合安全規(guī)則的前端數(shù)據(jù)庫(kù)與云存儲(chǔ)請(qǐng)求,保障數(shù)據(jù)和文件安全。
在前面我們建議使用安全規(guī)則取代簡(jiǎn)易版的權(quán)限設(shè)置,當(dāng)使用安全規(guī)則之后,這里有一個(gè)重要的核心就是 {openid} 變量 ,無(wú)論在前端(小程序端、web端)查詢時(shí),它都是必不可少的(也就是說(shuō)云函數(shù),云開發(fā)控制臺(tái)不受安全規(guī)則控制)。
{openid} 變量在小程序端使用時(shí)無(wú)需先通過(guò)云函數(shù)獲取用戶的 openid,直接使用'{openid}'
即可,而我們?cè)诓樵儠r(shí)都需要顯式傳入openid。之前我們使用簡(jiǎn)易權(quán)限配置時(shí)不需要這么做,這是因?yàn)椴樵儠r(shí)會(huì)默認(rèn)給查詢條件加上一條 _openid 必須等于用戶 openid,但是使用安全規(guī)則之后,就沒(méi)有這個(gè)默認(rèn)的查詢條件了。
比如我們?cè)诓樵僣ollection時(shí),都需要在where里面添加如下如下的條件,{openid}變量就會(huì)附帶當(dāng)前用戶的openid。
db.collection('china').where({
_openid: '{openid}', //安全規(guī)則里有auth.openid時(shí)都需要添加
})
更新、刪除等數(shù)據(jù)庫(kù)的寫入請(qǐng)求也都需要明確在where里添加這樣的一個(gè)條件(使用安全規(guī)則后,在小程序端也可以進(jìn)行批量更新和刪除)。
db.collection('goods').where({
_openid: '{openid}',
category: 'mobile'
}).update({ //批量更新
data:{
price: _.inc(1)
}
})
開啟安全規(guī)則之后,都需要在where查詢條件里指定
_openid: '{openid}'
,這是因?yàn)榇蠖鄶?shù)安全規(guī)則里都有auth.openid
,也就是對(duì)用戶的身份有要求,where查詢條件為安全規(guī)則的子集,所以都需要添加。當(dāng)然你也可以根據(jù)你的情況,安全規(guī)則不要求用戶的身份,也就可以不傳入_openid: '{openid}'
了。
由于我們?cè)谶M(jìn)行執(zhí)行doc操作db.collection('china').doc(id)
時(shí),沒(méi)法傳入openid的這個(gè)條件,那應(yīng)該怎么控制權(quán)限呢?這時(shí)候,我們可以把doc操作都轉(zhuǎn)化為where操作就可以了,在where查詢里指定 _id 的值,這樣就只會(huì)查詢到一條記錄了:
db.collection('china').where({
_id: 'tcb20200501', //條件里面加_id
_openid: '{openid}', //安全規(guī)則里有auth.openid時(shí)都需要添加
})
至于其他的doc操作,都需要轉(zhuǎn)化為基于collection的where操作,也就是說(shuō)以后不再使用doc操作db.collection('china').doc(id)
了。其中doc.update、doc.get和doc.remove可以用基于collection的update、get、remove取代,doc.set可以被更新指令_.set
取代。當(dāng)然安全規(guī)則只適用于前端(小程序端或Web端),后端不受安全規(guī)則的權(quán)限限制。
在使用簡(jiǎn)易權(quán)限配置時(shí),用戶在小程序端往數(shù)據(jù)庫(kù)里寫入數(shù)據(jù)時(shí),都會(huì)給記錄doc里添加一個(gè)_openid的字段來(lái)記錄用戶的openid,使用安全規(guī)則之后同樣也是如此。在創(chuàng)建記錄時(shí),可以把{openid}變量賦值給非_openid的字段或者寫入到嵌套數(shù)組里,后臺(tái)寫入記錄時(shí)發(fā)現(xiàn)該字符串時(shí)會(huì)自動(dòng)替換為小程序用戶的 openid:
db.collection('posts').add({
data:{
books:[{
title:"云開發(fā)快速入門",
author:'{openid}'
},{
title:"數(shù)據(jù)庫(kù)入門與實(shí)戰(zhàn)",
author:'{openid}'
}]
}
})
以往要進(jìn)行openid的寫入操作時(shí)需要先通過(guò)云函數(shù)返回用戶openid,使用安全規(guī)則之后,直接使用{openid}變量即可,不過(guò)該方法僅支持add添加一條記錄時(shí),不支持update的方式。
使用安全規(guī)則之后,我們可以在控制臺(tái)(開發(fā)者工具和網(wǎng)頁(yè))對(duì)每個(gè)集合以及云存儲(chǔ)的文件夾分別配置安全規(guī)則,也就是自定義權(quán)限,配置的格式是json,仍然嚴(yán)格遵循json配置文件的寫法(比如數(shù)組最后一項(xiàng)不能有逗號(hào),,配置文件里不能有注釋等)。
我們先來(lái)看簡(jiǎn)易權(quán)限配置所有用戶可讀,僅創(chuàng)建者可寫
、僅創(chuàng)建者可讀寫
、所有用戶可讀
、所有用戶不可讀寫
所對(duì)應(yīng)的安全規(guī)則的寫法,這個(gè)json配置文件的key
表示操作類型,value
是一個(gè)表達(dá)式,也是一個(gè)條件,解析為true時(shí)表示相應(yīng)的操作符合安全規(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
}
簡(jiǎn)易的權(quán)限配置只有讀read與寫write,而使用安全規(guī)則之后,支持權(quán)限操作有除了讀與寫外,還將寫權(quán)限細(xì)分為create新建、update更新、delete刪除,也就是既可以只使用寫,也可以細(xì)分為增、刪、改,比如下面的案例為 所有人可讀,創(chuàng)建者可寫可更新,但是不能刪除
"read": true,
"create":"auth.openid == doc._openid",
"update":"auth.openid == doc._openid",
"delete":false
操作類型無(wú)外乎增刪改查,不過(guò)安全規(guī)則的value是條件表達(dá)式,寫法很多,讓安全規(guī)則也就更加靈活。值得一提的是,如果我們不給read或者write賦值,它們的默認(rèn)值為false。
安全規(guī)則還可以配置所有人可讀可寫的類型,也就是如下的寫法,讓所有登錄用戶(用戶登錄了之后才有openid,即openid不為空)可以對(duì)數(shù)據(jù)可讀可寫。
{
"read": "auth.openid != null",
"write": "auth.openid != null"
}
在小程序端,我們可以把數(shù)據(jù)庫(kù)集合的安全規(guī)則操作read和write都寫為true(這是所有人可讀可寫,而這里強(qiáng)調(diào)的是所有用戶),因?yàn)橹灰脩羰褂瞄_啟了云開發(fā)的小程序,就會(huì)免鑒權(quán)登錄有了openid,但是上面安全規(guī)則的寫法則通用于云存儲(chǔ)、網(wǎng)頁(yè)端的安全規(guī)則。
集合里的數(shù)據(jù)讓所有用戶可讀可寫在很多方面都有應(yīng)用,尤其是我們希望有其他用戶可以對(duì)嵌套數(shù)組和嵌套對(duì)象里的字段進(jìn)行更新時(shí)。比如集合posts存儲(chǔ)的是所有資訊文章,而我們會(huì)把文章的評(píng)論嵌套在集合里。
{
_id:"tcb20200503112",
_openid:"用戶A", //用戶A也是作者,他發(fā)表的文章
title:"云開發(fā)安全規(guī)則的使用經(jīng)驗(yàn)總結(jié)",
stars:223,
comments:[{
_openid:"用戶B",
comment:"好文章,作者有心了",
}]
}
當(dāng)用戶A發(fā)表文章時(shí),也就會(huì)創(chuàng)建這條記錄,如果用戶B希望可以評(píng)論(往數(shù)組comments里更新數(shù)據(jù))、點(diǎn)贊文章(使用inc原子更新更新stars的值),就需要對(duì)該記錄可讀可寫(至少是可以更新)。這在簡(jiǎn)易權(quán)限配置是無(wú)法做到的(只能使用云函數(shù)來(lái)操作),有了安全規(guī)則之后,一條記錄就可以有被多個(gè)人同時(shí)維護(hù)的權(quán)限,而這樣的場(chǎng)景在云開發(fā)這種文檔型數(shù)據(jù)庫(kù)里比較常見(因?yàn)樯婕暗角短讛?shù)組嵌套對(duì)象)。
安全規(guī)則與查詢where里的條件是相互配合的,但是兩者之間又有一定的區(qū)別。所有安全規(guī)則的語(yǔ)句指向的都是符合條件的文檔記錄,而不是集合。使用了安全規(guī)則的where查詢會(huì)先對(duì)文檔進(jìn)行安全規(guī)則的匹配,比如小程序端使用where查詢不到記錄,就會(huì)報(bào)錯(cuò)
errCode: -502003 database permission denied | errMsg: Permission denied
,然后再進(jìn)行條件匹配,比如安全規(guī)則設(shè)置為所有人可讀時(shí),當(dāng)沒(méi)有符合條件的結(jié)果時(shí),會(huì)顯示查詢的結(jié)果為0。我們要注意無(wú)權(quán)查詢和查詢結(jié)果為0的區(qū)別。
要搞清楚安全規(guī)則寫法的意思,我們還需要了解一些全局變量,比如前面提及的auth.openid
表示的是登錄用戶的openid,而doc._openid
表示的是當(dāng)前記錄_openid
這個(gè)字段的值,當(dāng)用戶的openid與當(dāng)前記錄的_openid值相同時(shí),就對(duì)該記錄有權(quán)限。全局變量還有now(當(dāng)前時(shí)間戳)和resource(云存儲(chǔ)相關(guān))。
變量 | 類型 | 說(shuō)明 |
---|---|---|
auth | object | 用戶登錄信息,auth.openid 也就是用戶的openid,如果是在web端它還有l(wèi)oginType登錄方式、uid等值 |
doc | object | 表示當(dāng)前記錄的內(nèi)容,用于匹配記錄內(nèi)容/查詢條件 |
now | number | 當(dāng)前時(shí)間的時(shí)間戳,也就是以從計(jì)時(shí)原點(diǎn)開始計(jì)算的毫秒 |
resource | object | resource.openid為云存儲(chǔ)文件私有歸屬標(biāo)識(shí),標(biāo)記所有者的openid |
安全規(guī)則的表達(dá)式還支持運(yùn)算符,比如等于==
,不等于!=
,大于>
,大于等于>=
,小于<
,小于等于<=
,與&&
,或||
等等,后面會(huì)有具體的介紹。
運(yùn)算符 | 說(shuō)明 | 示例 | |
---|---|---|---|
== | 等于 | 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']中的一個(gè) |
!(xx in []) | 不存在在集合中,使用 in 的方式描述 !(a in [1,2,3]) |
!(auth.openid in ['zzz','aaa']) | 用戶的 openid 不是['zzz','aaa']中的任何一個(gè) |
&& | 與 | auth.openid == 'zzz' && doc.age>10 | 用戶的 openid 為 zzz 并且查詢條件的 age 屬性大于 10 |
|| | 或 | auth.openid == 'zzz'|| doc.age>10 | 用戶的 openid 為 zzz 或者查詢條件的 age 屬性大于 10 |
. | 對(duì)象元素訪問(wèn)符 | auth.openid | 用戶的 openid |
[] | 數(shù)組訪問(wèn)符屬性 | doc.favorites[0] == 'zzz' | 查詢條件的 favorites 數(shù)組字段的第一項(xiàng)的值等于 zzz |
全局變量auth與doc的組合使用可以讓登錄用戶的權(quán)限依賴于記錄的某個(gè)字段,auth表示的是登錄用戶,而doc、resource則是云開發(fā)環(huán)境的資源相關(guān),使用安全規(guī)則之后用戶與數(shù)據(jù)庫(kù)、云存儲(chǔ)之間就有了聯(lián)系。resource只有resource.openid,而doc不只有_openid,還可以有很多個(gè)字段,也就讓數(shù)據(jù)庫(kù)的權(quán)限有了很大的靈活性,后面我們更多的是以doc全局變量為例。
auth.openid是當(dāng)前的登錄用戶,而記錄doc里的openid則可以讓該記錄與登錄用戶之間有緊密的聯(lián)系,或者可以說(shuō)讓該記錄有了一個(gè)身份的驗(yàn)證。一般來(lái)說(shuō)doc._openid所表示的是該記錄的創(chuàng)建者的openid,簡(jiǎn)易權(quán)限控制比較的也是當(dāng)前登錄用戶是否是該記錄的創(chuàng)建者(或者為更加開放且粗放的權(quán)限)。
//登錄用戶為記錄的創(chuàng)建者時(shí),才有權(quán)限讀
"read": "auth.openid == doc._openid",
//不允許記錄的創(chuàng)建者刪除記錄(只允許其他人刪除)
"delete": "auth.openid != doc._openid",
安全規(guī)則和where查詢是配套使用的,如果你指定記錄的權(quán)限與創(chuàng)建者的openid有關(guān),你在前端的查詢條件的范圍就不能比安全規(guī)則的大(如果查詢條件的范圍比安全規(guī)則的范圍大就會(huì)出現(xiàn)database permission denied
:
db.collection('集合id').where({
_openid:'{openid}' //有doc._openid,因此查詢條件里就需要有_openid這個(gè)條件,
key:"value"
})
.get().then(res=>{
console.log(res)
})
1、把權(quán)限指定給某個(gè)人
安全規(guī)則的身份驗(yàn)證則不會(huì)局限于記錄的創(chuàng)建者,登錄用戶的權(quán)限還可以依賴記錄的其他字段,我們還可以給記錄的權(quán)限指定為某一個(gè)人(非記錄的創(chuàng)建者),比如很多個(gè)學(xué)生提交了作業(yè)之后,會(huì)交給某一個(gè)老師審閱批改,老師需要對(duì)該記錄有讀寫的權(quán)限,在處理時(shí),可以在學(xué)生提交作業(yè)(創(chuàng)建記錄doc)時(shí)時(shí)可以指定teacher的openid,只讓這個(gè)老師可以批閱,下面是文檔的結(jié)構(gòu)和安全規(guī)則示例:
//文檔的結(jié)構(gòu)
{
_id:"handwork20201020",
_openid:"學(xué)生的openid", //學(xué)生為記錄的創(chuàng)建者,
teacher:"老師的openid" //該學(xué)生被指定的老師的openid
}
//安全規(guī)則
{
"read": "doc.teacher == auth.openid || doc._openid == auth.openid",
"write": "doc.teacher == auth.openid || doc._openid == auth.openid",
}
讓登錄用戶auth.openid依賴記錄的其他字段,在功能表現(xiàn)上相當(dāng)于給該記錄指定了一個(gè)角色,如直屬老師、批閱者、直接上級(jí)、閨蜜、夫妻、任務(wù)的直接指派等角色。
對(duì)于查詢或更新操作,輸入的where查詢條件必須是安全規(guī)則的子集,比如你的安全規(guī)則如果是
doc.teacher == auth.openid
,而你在where里沒(méi)有teacher:'{openid}'
這樣的條件,就會(huì)出現(xiàn)權(quán)限報(bào)錯(cuò)。
由于安全規(guī)則和where查詢需要配套使用,安全規(guī)則里有doc.teacher
和doc._openid
,在where里也就需要寫安全規(guī)則的子集條件,比如_openid:'{openid}'
或teacher:'{openid}'
,由于這里老師也是用戶,我們可以傳入如下條件讓學(xué)生和老師共用一個(gè)數(shù)據(jù)庫(kù)請(qǐng)求:
const db = wx.cloud.database()
const _ = db.command
//一條記錄可以同時(shí)被創(chuàng)建者(學(xué)生)和被指定的角色(老師)讀取
db.collection('集合id').where(_.or([
{_openid:'{openid}' }, //與安全規(guī)則doc._openid == auth.openid對(duì)應(yīng)
{teacher:'{openid}' } //與安全規(guī)則doc.teacher == auth.openid對(duì)應(yīng)
]))
.get().then(res=>{
console.log(res)
})
2、把權(quán)限指定給某些人
上面的這個(gè)角色指定是一對(duì)一、或多對(duì)一的指定,也可以是一對(duì)多的指定,可以使用in
或!(xx in [])
運(yùn)算符。比如下面是可以給一個(gè)記錄指定多個(gè)角色(學(xué)生創(chuàng)建的記錄,多個(gè)老師有權(quán)讀寫):
//文檔的結(jié)構(gòu)
{
_id:"handwork20201020",
_openid:"學(xué)生的openid", //學(xué)生為記錄的創(chuàng)建者,
teacher:["老師1的openid","老師2的openid","老師3的openid"]
}
//安全規(guī)則
{
"read": "auth.openid in doc.teacher || doc._openid == auth.openid",
"write": "auth.openid in doc.teacher || doc._openid == auth.openid",
}
這里要再?gòu)?qiáng)調(diào)的是前端(小程序端)的where條件必須是安全規(guī)則權(quán)限的子集,比如我們?cè)谛〕绦蚨酸槍?duì)老師進(jìn)行如下查詢('{openid}'
不支持查詢指令,需要后端獲取)
db.collection('集合id').where({
_openid:'{openid}',
teacher:_.elemMatch(_.eq('老師的openid'))
}).get()
.then(res=>{
console.log(res)
})
前面我們實(shí)現(xiàn)了將記錄的權(quán)限指定給某個(gè)人或某幾個(gè)人,那如何將記錄的權(quán)限指定給某類人呢?比如打車軟件為了數(shù)據(jù)的安全性會(huì)有司機(jī)、乘客、管理員、開發(fā)人員、運(yùn)維人員、市場(chǎng)人員等,這都需要我們?cè)跀?shù)據(jù)庫(kù)里新建一個(gè)字段來(lái)存儲(chǔ)用戶的類型,比如
{role:3}
,用1、2、3、4等數(shù)字來(lái)標(biāo)明,或者用{isManager:true}
boolean類型來(lái)標(biāo)明,這個(gè)新增的字段可以就在查詢的集合文檔里doc.role
,或者是一個(gè)單獨(dú)的集合(也就是存儲(chǔ)權(quán)限的集合和要查詢的集合是分離的,這需要使用get函數(shù)跨集合查詢),后面會(huì)有具體介紹。
3、doc.auth與文檔的創(chuàng)建者
下面有一個(gè)例子可以加深我們對(duì)安全規(guī)則的理解,比如我們?cè)谟涗浝镏付ㄎ臋n的auth為其他人的openid,并配上與之相應(yīng)的安全規(guī)則,即使當(dāng)前用戶實(shí)際上就是這個(gè)記錄的創(chuàng)建者,這個(gè)記錄有該創(chuàng)建者的_openid,他也沒(méi)有操作的權(quán)限。安全規(guī)則會(huì)對(duì)查詢條件進(jìn)行評(píng)估,只要符合安全規(guī)則,查詢才會(huì)成功,違反安全規(guī)則,查詢就會(huì)失敗。
//文檔的結(jié)構(gòu),比如以下為一條記錄
{
_id:"handwork20201020",
_openid:"創(chuàng)建者的openid",
auth:"指定的auth的openid"
}
//安全規(guī)則
{
"權(quán)限操作": "auth.openid == doc.auth" //權(quán)限操作為read、write、update等
}
//前端查詢,不符合安全規(guī)則,即使是記錄的創(chuàng)建者也沒(méi)有權(quán)限
db.collection('集合id').where({
auth:'{openid}'
})
簡(jiǎn)易版權(quán)限設(shè)置沒(méi)法在前端實(shí)現(xiàn)記錄跨用戶的寫權(quán)限(含update、create、delete),也就是說(shuō)記錄只有創(chuàng)建者可寫。而文檔型數(shù)據(jù)庫(kù)一個(gè)記錄因?yàn)榉捶妒交短椎脑蚩梢猿休d的信息非常多,B用戶操作A用戶創(chuàng)建的記錄,尤其是使用更新指令update字段以及內(nèi)嵌字段的值這樣的場(chǎng)景是非常常見的。除此之外,僅安全規(guī)則可以實(shí)現(xiàn)前端對(duì)記錄的批量更新和刪除。
比如我們可以把評(píng)論、收藏、點(diǎn)贊、轉(zhuǎn)發(fā)、閱讀量等信息內(nèi)嵌到文章的集合里,以往我們?cè)谛〕绦蚨耍ㄖ荒芡ㄟ^(guò)云函數(shù))是沒(méi)法讓B用戶對(duì)A用戶創(chuàng)建的記錄進(jìn)行操作,比如點(diǎn)贊、收藏、轉(zhuǎn)發(fā)時(shí)用更新指令inc更新次數(shù),比如沒(méi)法直接用更新指令將評(píng)論push寫入到記錄里:
{
_id:"post20200515001",
title:"云開發(fā)安全規(guī)則實(shí)戰(zhàn)",
star:221, //點(diǎn)贊數(shù)
comments:[{ //評(píng)論和子評(píng)論
content:"安全規(guī)則確實(shí)是非常好用",
nickName:"小明",
subcomment:[{
content:"我也這么覺(jué)得",
nickName:"小軍",
}]
}],
share:12, //轉(zhuǎn)發(fā)數(shù)
collect:15 //收藏?cái)?shù)
readNum:2335 //閱讀量
}
在開啟安全規(guī)則,我們就可以直接在前端讓B用戶修改A用戶創(chuàng)建的記錄,這樣用戶閱讀、點(diǎn)贊、評(píng)論、轉(zhuǎn)發(fā)、收藏文章等時(shí),就可以直接使用更新指令對(duì)文章進(jìn)行字段級(jí)別的更新。
"read":"auth.openid != null",
"update":"auth.openid != null"
這個(gè)安全規(guī)則相比于所有人可讀,僅創(chuàng)建者可讀寫
,開放了update的權(quán)限,小程序端也有l(wèi)imit 20的限制。而如果不使用安全規(guī)則,把這些放在云函數(shù)里進(jìn)行處理不僅處理速度更慢,而且非常消耗云函數(shù)的資源。
db.collection('post').where({
_id:"post20200515001",
openid:'{openid}'
}).update({
data:{
//更新指令的應(yīng)用
}
})
我們還可以把訪問(wèn)權(quán)限的控制信息以字段的形式存儲(chǔ)在數(shù)據(jù)庫(kù)的集合文檔里,而安全規(guī)則可以根據(jù)文檔數(shù)據(jù)動(dòng)態(tài)地允許或拒絕訪問(wèn),也就是說(shuō)doc的規(guī)則匹配可以讓記錄的權(quán)限動(dòng)態(tài)依賴于記錄的某一個(gè)字段的值。
doc規(guī)則匹配的安全規(guī)則針對(duì)的是整個(gè)集合,而且要求集合里的所有記錄都有相應(yīng)的權(quán)限字段,而只有在權(quán)限字段滿足一定條件時(shí),記錄才有權(quán)限被增刪改查,是一個(gè)將集合的權(quán)限范圍按照條件要求收窄的過(guò)程,where查詢時(shí)的條件不能比安全規(guī)則規(guī)定的范圍大(查詢條件為安全規(guī)則子集);配置了安全規(guī)則的集合里的記錄只有兩種狀態(tài),有權(quán)限和沒(méi)有權(quán)限。
這里仍然再?gòu)?qiáng)調(diào)的是使用where查詢時(shí)要求查詢條件是安全規(guī)則的子集,在進(jìn)行where查詢前會(huì)先解析規(guī)則與查詢條件進(jìn)行校驗(yàn),如果where條件不是安全規(guī)則的子集就會(huì)出現(xiàn)權(quán)限報(bào)錯(cuò),不能把安全規(guī)則看成是一個(gè)篩選條件,而是一個(gè)保護(hù)記錄數(shù)據(jù)安全的不可逾越的規(guī)則。
doc的規(guī)則匹配,特別適合每個(gè)記錄存在多個(gè)狀態(tài)或每個(gè)記錄都有一致的權(quán)限條件(要么全部是,要么全部否),而只有一個(gè)狀態(tài)或滿足條件才有權(quán)限被用戶增刪改查時(shí)的情形,比如文件審批生效(之前存在審批沒(méi)有生效的多個(gè)狀態(tài)),文章的發(fā)布狀態(tài)為pubic(之前為private或其他狀態(tài)),商品的上架(在上架前有多個(gè)狀態(tài)),文字圖片內(nèi)容的安全檢測(cè)不違規(guī)(之前在進(jìn)行后置校驗(yàn)),消息是否撤回,文件是否刪除,由于每個(gè)記錄我們都需要標(biāo)記權(quán)限,而只有符合條件的記錄才有被增刪改查的機(jī)會(huì)。
比如資訊文章的字段如下,每個(gè)記錄對(duì)應(yīng)著一篇文章,而status則存儲(chǔ)著文章的多個(gè)狀態(tài),只有public時(shí),文章才能被用戶查閱到,我們可以使用安全規(guī)則"read": "doc.status=='public'"
。而對(duì)于軟刪除(文章假刪除),被刪除可以作為一個(gè)狀態(tài),但是文章還是在數(shù)據(jù)庫(kù)里。
{
_id:"post2020051314",
title:"云開發(fā)發(fā)布新能力,支持微信支付云調(diào)用",
status:"public"
},
{
_id:"post2020051312",
title:"云函數(shù)灰度能力上線",
status:"edit"
},
{
_id:"post2020051311",
title:"云開發(fā)安全規(guī)則深度研究",
status:"delete"
}
而在前端(小程序端)與之對(duì)應(yīng)的數(shù)據(jù)庫(kù)查詢條件則必須為安全規(guī)則的子集,也就是說(shuō)安全規(guī)則不能作為你查詢的過(guò)濾條件,安全規(guī)則會(huì)對(duì)查詢進(jìn)行評(píng)估,如果查詢不符合安全規(guī)則設(shè)置的約束(非子集),那么前端的查詢請(qǐng)求沒(méi)有權(quán)限讀取文檔,而不是過(guò)濾文檔:
db.collection('集合id').where({
status:"public" //你不能不寫這個(gè)條件,而指望安全規(guī)則給你過(guò)濾
}).get()
.then(res=>{
console.log(res)
})
有時(shí)候我們需要對(duì)某些記錄有著非常嚴(yán)格的要求,禁止為空,如何為空一律不予被前端增刪改查,比如已經(jīng)上架的shop集合里的商品列表,有些核心數(shù)據(jù)如價(jià)格、利潤(rùn)、庫(kù)存等就不能為空,給企業(yè)造成損失,相應(yīng)的安全規(guī)則和查詢?nèi)缦拢?/p>
//安全規(guī)則
{
"權(quán)限操作": "doc.profit != null",
}
//權(quán)限操作,profit = 0.65就是安全規(guī)則的子集
db.collection('shop').where({
profit:_.eq(0.65)
})
安全規(guī)則記錄的字段值不僅限于一個(gè)狀態(tài)(字符串類型),還可以是可以運(yùn)算的范圍值,如大于>
,小于<
、in
等,比如商品的客單價(jià)都是100以上,管理員在后端(控制臺(tái),云函數(shù)等)把原本190元的價(jià)格寫成了19,或者失誤把價(jià)格寫成了負(fù)數(shù),這種情況下我們對(duì)商品集合使用安全規(guī)則doc.price > 100
,前端將失去所有價(jià)格低于100的商品的操作權(quán)限,包括查詢。
//安全規(guī)則
"操作權(quán)限":"doc.price > 100"
//相應(yīng)的查詢
db.collection('shop').where({
price:_eq(125)
})
安全規(guī)則的全局變量now表示的是當(dāng)前時(shí)間的時(shí)間戳,這讓安全規(guī)則可以給權(quán)限的時(shí)間節(jié)點(diǎn)和權(quán)限的時(shí)效性設(shè)置一些規(guī)則,這里就不具體講述了。
全局函數(shù)get可以實(shí)現(xiàn)跨集合來(lái)限制權(quán)限。doc的權(quán)限匹配更多的是基于文檔性質(zhì)的權(quán)限,也就是集合內(nèi)所有文檔都有相同的字段,根據(jù)這個(gè)字段的值的不同來(lái)劃分權(quán)限。但是有時(shí)候我們希望實(shí)現(xiàn)多個(gè)用戶和多個(gè)用戶角色來(lái)管理集合的文檔,擁有不同的權(quán)限,如果把用戶和角色都寫進(jìn)文檔的每個(gè)記錄里,就會(huì)非常難以管理。也就是說(shuō)doc的權(quán)限匹配并不適合復(fù)雜的用戶管理文檔的權(quán)限體系。
我們可以把單個(gè)復(fù)雜的集合文檔(反范式化的設(shè)計(jì))拆分成多個(gè)集合文檔(范式化設(shè)計(jì)),將用戶和角色從文檔里分離出來(lái)。比如博客有文章post集合,而user集合除了可以把用戶劃分為作者、編輯、投稿者這樣的用戶身份,還可以是管理員組,編輯組等。如果我們把記錄的權(quán)限賦予給的人員比較多或群組比較復(fù)雜,則需要把角色存儲(chǔ)在其獨(dú)立的集合中,而不是作為目標(biāo)文檔中的一個(gè)字段,用全局函數(shù)get來(lái)實(shí)現(xiàn)跨集合的權(quán)限限制。
get 函數(shù)是全局函數(shù),可以跨集合來(lái)獲取指定的記錄,用于在安全規(guī)則中獲取跨集合的記錄來(lái)參與到安全規(guī)則的匹配中,get函數(shù)的參數(shù)格式是 database.集合名.記錄id
。
比如我們可以給文章post集合設(shè)置如下安全規(guī)則,只有管理員才可以刪除記錄,而判斷用戶是否為管理員則需要跨集合用user集合里的字段值來(lái)判斷:
//user集合的結(jié)構(gòu)
{
_id:"oUL-m5FuRmuVmxvbYOGuXbuEDsn8", //用戶的openid
isManager:true
}
//post集合的權(quán)限
{
"read": "true",
"delete": "get(`database.user.${auth.openid}`).isManager== true"
}
db.collection('post').where({
//相應(yīng)的條件,并不受子集的限制
})
get函數(shù)還可以接收變量,值可以通過(guò)多種計(jì)算方式得到,例如使用字符串模版進(jìn)行拼接,這是一個(gè)查詢的過(guò)程,如果相應(yīng)的文檔里有記錄,則函數(shù)返回記錄的內(nèi)容,否則返回空(注意反引號(hào)的寫法):
`(database.${doc.collction}.${doc._id})`
get函數(shù)的限制條件
讀操作觸發(fā)與配額消耗說(shuō)明
get 函數(shù)的執(zhí)行會(huì)計(jì)入數(shù)據(jù)庫(kù)請(qǐng)求數(shù),同樣受數(shù)據(jù)庫(kù)配額限制。在未使用變量的情況下,每個(gè) get 會(huì)產(chǎn)生一次讀操作,在使用變量時(shí),對(duì)每個(gè)變量值會(huì)產(chǎn)生一次 get 讀操作。例如:
假設(shè)某集合 shop 上有如下規(guī)則:
{
"read": "auth.openid == get(`database.shop.${doc._id}`).owner",
"write": false
}
在執(zhí)行如下查詢語(yǔ)句時(shí)會(huì)產(chǎn)生 5 次讀取。
db.collection('shop').where(_.or([{_id:1},{_id:2},{_id:3},{_id:4},{_id:5}])).get()
更多建議: