[TOC]
## [簡介](#簡介)
數據安全在應用開發的任何階段都應該被重視。因此本文檔我們詳細介紹了在選擇 LeanCloud 作為后端服務之后,如何使用 LeanCloud 提供的安全功能模塊為開發者的應用以及數據提供安全保障。
如果您尚未對權限管理以及 ACL 的進行過了解,請查看 權限管理以及 ACL 快速指南](acl_quick_start-js.html)。
## [需求場景](#需求場景)
列舉一個場景: 假設我們要做一個極簡的論壇:用戶只能修改或者刪除自己發的帖子,其他用戶則只能查看。
### [云引擎使用 ACL](#云引擎使用_ACL)
文檔中使用的?`AV.User.current()`?這個方法僅僅針對瀏覽器端有效,在云引擎中該接口無法使用。云引擎中獲取用戶信息,請參考?[云引擎指南 · 處理用戶登錄和登出](https://leancloud.cn/docs/leanengine_webhosting_guide-node.html#處理用戶登錄和登出)。
## [基于用戶的權限管理](#基于用戶的權限管理)
### [單用戶權限設置](#單用戶權限設置)
以上需求在 LeanCloud 中實現的步驟如下:
1. 寫一篇帖子
2. 設置帖子的「讀」權限為所有人可讀。
3. 設置帖子的「寫」權限為作者可寫。
4. 保存帖子
實例代碼如下:
~~~
// 新建一個帖子對象
var Post = AV.Object.extend('Post');
var post = new Post();
post.set('title', '大家好,我是新人');
// 新建一個 ACL 實例
var acl = new AV.ACL();
acl.setPublicReadAccess(true);
acl.setWriteAccess(AV.User.current(),true);
// 將 ACL 實例賦予 Post 對象
post.setACL(acl);
post.save().then(function() {
// 保存成功
}).catch(function(error) {
console.log(error);
});
~~~
以上代碼產生的效果在?控制臺?>?存儲?>?Post 表?可以看到,這條記錄的 ACL 列上的值為:
~~~
{"*":{"read":true},"55b9df0400b0f6d7efaa8801":{"write":true}}
~~~
此時,這種 ACL 值的表示:所有用戶均有「讀」權限,而?`objectId`?為?`55b9df0400b0f6d7efaa8801`?擁有「寫」權限,其他用戶不具備「寫」權限。
### [多用戶權限設置](#多用戶權限設置)
假如需求增加為:帖子的作者允許某個特定的用戶可以修改帖子,除此之外的其他人不可修改。 實現步驟就是額外指定一個用戶,為他設置帖子的「寫」權限:
注意:開啟?`_User`?表的查詢權限才可以執行以下代碼
~~~
// 創建一個針對 User 的查詢
var query = new AV.Query(AV.User);
query.get('55098d49e4b02ad5826831f6').then(function(otherUser) {
var post = new AV.Object('Post');
post.set('title', '大家好,我是新人');
// 新建一個 ACL 實例
var acl = new AV.ACL();
acl.setPublicReadAccess(true);
acl.setWriteAccess(AV.User.current(), true);
acl.setWriteAccess(otherUser, true);
// 將 ACL 實例賦予 Post 對象
post.setACL(acl);
// 保存到云端
return post.save();
}).then(function() {
// 保存成功
}).catch(function(error) {
// 錯誤信息
console.log(error);
});
~~~
執行完畢上面的代碼,回到控制臺,可以看到,該條 Post 記錄里面的 ACL 列的內容如下:
~~~
{"*":{"read":true},"55b9df0400b0f6d7efaa8801":{"write":true},"55f1572460b2ce30e8b7afde":{"write":true}}
~~~
從結果可以看出,該條 Post 已經允許 Id 為?`55b9df0400b0f6d7efaa8801`?以及?`55f1572460b2ce30e8b7afde`?兩個用戶(AVUser)可以修改,他們擁有?`write:ture`?的權限,也就是「寫」權限。
基于用戶的權限管理比較簡單直接,開發者理解起來成本較低。
### [局限性](#局限性)
再進一步的場景: 論壇升級,需要一個特定的管理員(Administrator)來統一管理論壇的帖子,他可以修改帖子的內容,刪除不合適的帖子。
論壇升級之后,用戶發布帖子的步驟需要針對上一小節做如下調整:
1. 寫一篇帖子
2. 設置帖子的「讀」權限為所有人。
3. 設置帖子的「寫」權限為作者以及管理員
4. 保存帖子
我們可以設想一下,每當論壇產生一篇帖子,就得為管理員添加這篇帖子的「寫」權限。
假如做權限管理功能的時候都依賴基于用戶的權限管理,那么一旦產生變化就會發現這種實現方式的局限性。
比如新增了一個管理員,新的管理員需要針對目前論壇所有的帖子擁有管理員應有的權限,那么我們需要把數據庫現有的所有帖子循環一遍,為新的管理員增加「寫」權限。
假如論壇又一次升級了,付費會員享有特殊帖子的讀權限,那么我們需要在發布新帖子的時候,設置「讀」權限給部分人(付費會員)。這需要查詢所有付費會員并一一設置。
毫無疑問,這種實現方式是完全失控的,基于用戶的權限管理,在針對簡單的私密分享類的應用是可行的,但是一旦產生需求變更,這種實現方式是不被推薦的。
## [基于角色的權限管理](#基于角色的權限管理)
管理員,會員,普通用戶這三種概念在程序設計中,被定義為「角色」。 我們可以看出,在列出的需求場景中,「權限」的作用是用來區分某一數據是否允許某種角色的用戶進行操作。
> 「權限」只和「角色」對應,而用戶也和「角色」對應,為用戶賦予「角色」,然后管理「角色」的權限,完成了權限與用戶的解耦。
>
>
>
> +
>
>
因此我們來解釋 LeanCloud 中「權限」和「角色」的概念。
「權限」在 LeanCloud 服務端只存在兩種權限:讀、寫。 「角色」在 LeanCloud 服務端沒有限制,唯一要求的就是在一個應用內,角色的名字唯一即可,至于某一個「角色」在當前應用內對某條數據是否擁有讀寫的「權限」應該是有開發者的業務邏輯決定,而 LeanCloud 提供了一系列的接口幫助開發者快速實現基于角色的權限管理。
為了方便開發者實現基于角色的權限管理,LeanCloud 在 SDK 中集成了一套完整的 ACL (Access Control List) 系統。通俗的解釋就是為每一個數據創建一個訪問的白名單列表,只有在名單上的用戶(AVUser)或者具有某種角色(AVRole)的用戶才能被允許訪問。
為了更好地保證用戶數據安全性, LeanCloud 表中每一張都有一個 ACL 列。當然,LeanCloud 還提供了進一步的讀寫權限控制。
一個 User 必須擁有讀權限(或者屬于一個擁有讀權限的 Role)才可以獲取一個對象的數據,同時,一個 User 需要寫權限(或者屬于一個擁有寫權限的 Role)才可以更改或者刪除一個對象。下面列舉幾種常見的 ACL 使用范例。
### [ACL 權限管理](#ACL_權限管理)
#### [默認權限](#默認權限)
在沒有顯式指定的情況下,LeanCloud 中的每一個對象都會有一個默認的 ACL 值。這個值代表了所有的用戶對這個對象都是可讀可寫的。此時你可以在數據管理的表中 ACL 屬性中看到這樣的值:
~~~
{"*":{"read":true,"write":true}}
~~~
在?[基于用戶的權限管理](#基于用戶的權限管理)?的章節中,已經在代碼里面演示了通過 ACL 來實現基于用戶的權限管理,那么基于角色的權限管理也是依賴 ACL 來實現的,只是在介紹詳細的操作之前需要介紹「角色」這個重要的概念。
### [角色的權限管理](#角色的權限管理)
#### [角色的創建](#角色的創建)
首先,我們來創建一個?`Administrator`?的角色。
這里有一個需要特別注意的地方,因為?`AVRole`?本身也是一個?`AVObject`,它自身也有 ACL 控制,并且它的權限控制應該更嚴謹,如同「論壇的管理員有權力任命版主,而版主無權任命管理員」一樣的道理,所以創建角色的時候需要顯式地設定該角色的 ACL,而角色是一種較為穩定的對象:
~~~
// 新建一個角色,并把為當前用戶賦予該角色
var roleAcl = new AV.ACL();
roleAcl.setPublicReadAccess(true);
roleAcl.setPublicWriteAccess(false);
// 當前用戶是該角色的創建者,因此具備對該角色的寫權限
roleAcl.setWriteAccess(AV.User.current(), true);
//新建角色
var administratorRole = new AV.Role('Administrator', roleAcl);
administratorRole.save().then(function(role) {
// 創建成功
}).catch(function(error) {
console.log(error);
});
~~~
執行完畢之后,在控制臺可以查看?`_Role`?表里已經存在了一個?`Administrator`?的角色。 另外需要開發者注意的是:可以直接通過?控制臺?>?Post 表?>?其他?>?權限設置?直接設置權限。并且我們要強調的是:
> ACL 可以精確到 Class,也可以精確到具體的每一個對象(表中的每一條記錄)。
>
>
>
> +
>
>
#### [為對象設置角色的訪問權限](#為對象設置角色的訪問權限)
我們現在已經創建了一個有效的角色,接下來為?`Post`?對象設置?`Administrator`?的訪問「可讀可寫」的權限,設置成功以后,任何具備?`Administrator`?角色的用戶都可以對?`Post`?對象進行「可讀可寫」的操作了:
~~~
// 新建一個帖子對象
var Post = AV.Object.extend('Post');
var post = new Post();
post.set('title', '大家好,我是新人');
// 新建一個角色,并把為當前用戶賦予該角色
var administratorRole = new AV.Role('Administrator');
var relation = administratorRole.getUsers();
//為當前用戶賦予該角色
administratorRole.getUsers().add(AV.User.current());
//角色保存成功
administratorRole.save().then(function(administratorRole) {
// 新建一個 ACL 實例
var acl = new AV.ACL();
acl.setPublicReadAccess(true);
acl.setRoleWriteAccess(administratorRole, true);
// 將 ACL 實例賦予 Post 對象
post.setACL(acl);
return post.save();
}).then(function(post) {
// 保存成功
}).catch(function(error) {
// 保存失敗
console.log(error);
});
~~~
#### [用戶角色的賦予和剝奪](#用戶角色的賦予和剝奪)
經過以上兩步,我們還差一個給具體的用戶設置角色的操作,這樣才可以完整地實現基于角色的權限管理。
在通常情況下,角色和用戶之間本是多對多的關系,比如需要把某一個用戶提升為某一個版塊的版主,亦或者某一個用戶被剝奪了版主的權力,以此類推,在應用的版本迭代中,用戶的角色都會存在增加或者減少的可能,因此,LeanCloud 也提供了為用戶賦予或者剝奪角色的方式。 注意:在代碼級別,為角色添加用戶?與?為用戶賦予角色?實現的代碼是一樣的。 此類操作的邏輯順序是:
* 賦予角色:首先判斷該用戶是否已經被賦予該角色,如果已經存在則無需添加,如果不存在則為該用戶(AVUser)的?`roles`?屬性添加當前角色實例。
以下代碼演示為當前用戶添加?`Administrator`角色:
~~~
// 構建 AV.Role 的查詢
var administratorRole= //假設 administratorRole 是之前創建的 「Administrator」 角色;
var roleQuery = new AV.Query(AV.Role);
// 角色名稱等于 Administrator
roleQuery.equalTo('name', 'Administrator');
// 檢查當前用戶是否已經擁有了 Administrator 角色
roleQuery.equalTo('users', AV.User.current());
roleQuery.find().then(function (results) {
if (results.length > 0) {
// 當前用戶已經具備了 Administrator 角色,因此不需要做任何操作
var administratorRole = results[0];
return administratorRole;
} else {
// 當前用戶不具備 Administrator,因此你需要把當前用戶添加到 Role 的 Users 中
var relation = administratorRole.getUsers();
relation.add(AV.User.current());
return administratorRole.save();
}
}).then(function (administratorRole) {
//此時 administratorRole 已經包含了當前用戶
}).catch(function (error) {
// 輸出錯誤
console.log(error);
});
~~~
角色賦予成功之后,基于角色的權限管理的功能才算完成。
另外,此處不得不提及的就是角色的剝奪:
* 剝奪角色: 首先判斷該用戶是否已經被賦予該角色,如果未曾賦予則不做修改,如果已被賦予,則從對應的用戶(AVUser)的?`roles`?屬性當中把該角色刪除。
~~~
// 構建 AV.Role 的查詢
var roleQuery = new AV.Query(AV.Role);
roleQuery.equalTo('name', 'Moderator');
roleQuery.find().then(function(results) {
// 如果角色存在
if (results.length > 0) {
var moderatorRole = results[0];
roleQuery.equalTo('users', AV.User.current());
return roleQuery.find();
}
}).then(function(userForRole) {
//該角色存在,并且也擁有該角色
if (userForRole.length > 0) {
// 剝奪角色
var relation= moderatorRole.getUsers();
relation.remove(AV.User.current());
return moderatorRole.save();
}
}).then(function() {
// 保存成功
}).catch(function(error) {
// 輸出錯誤
console.log(error);
});
~~~
#### [角色的查詢](#角色的查詢)
除了在控制臺可以直接查看已有角色之外,通過代碼也可以直接查詢當前應用中已存在的角色。 注:`AVRole`?也繼承自?`AVObject`,因此熟悉了解?`AVQuery`?的開發者可以熟練的掌握關于角色查詢的各種方法。
~~~
// 新建針對 Role 的查詢
var roleQuery = new AV.Query(AV.Role);
// 查詢 name 等于 Administrator 的角色
roleQuery.equalTo('name', 'Administrator');
// 執行查詢
roleQuery.first().then(function(adminRole) {
var userRelation = adminRole.relation('users');
return userRelation.query().find();
}).then(function (userList) {
// userList 就是擁有該角色權限的所有用戶了。
var firstAdmin = userList[0];
}).catch(function(error) {
console.log(error);
});
~~~
查詢某一個用戶擁有哪些角色:
~~~
//第一種是通過 AV.User 的內置接口:
user.getRoles().then(function(roles){
// roles 是一個 AV.Role 數組,這些 AV.Role 表示 user 擁有的角色
});
// 第二種是通過查詢:
// 新建角色查詢
var roleQuery = new AV.Query(AV.Role);
// 查詢當前用戶擁有的角色
roleQuery.equalTo('users', AV.User.current());
roleQuery.find().then(function(roles) {
// roles 是一個 AV.Role 數組,這些 AV.Role 表示當前用戶所擁有的角色
}, function (error) {
});
~~~
查詢哪些用戶都被賦予?`Moderator`?角色:
~~~
var roleQuery = new AV.Query(AV.Role);
roleQuery.get('55f1572460b2ce30e8b7afde').then(function(role) {
//獲取 Relation 實例
var userRelation= role.getUsers();
// 獲取查詢實例
var query = userRelation.query();
return query.find();
}).then(function(results) {
// results 就是擁有 role 角色的所有用戶了
}).catch(function(error) {
console.log(error);
});
~~~
#### [角色的從屬關系](#角色的從屬關系)
角色從屬關系是為了實現不同角色的權限共享以及權限隔離。
權限共享很好理解,比如管理員擁有論壇所有板塊的管理權限,而版主只擁有單一板塊的管理權限,如果開發一個版主使用的新功能,都要同樣的為管理員設置該項功能權限,代碼就會冗余,因此,我們通俗的理解是:管理員也是版主,只是他是所有板塊的版主。因此,管理員在角色從屬的關系上是屬于版主的,只不過 TA 是特殊的版主。
~~~
// 建立版主和論壇管理員之間的從屬關系
var administratorRole = new AV.Role('Administrator');
var administratorRole.save().then(function(administratorRole) {
//新建版主角色
var moderatorRole = new AV.Role('Moderator');
// 將 Administrator 作為 moderatorRole 子角色
moderatorRole.getRoles().add(administratorRole);
return moderatorRole.save();
}).then(function (role) {
chai.assert.isNotNull(role.id);
done();
}).catch(function(error) {
console.log(error);
});
~~~
權限隔離也就是兩個角色不存在從屬關系,但是某些權限又是共享的,此時不妨設計一個中間角色,讓前面兩個角色從屬于中間角色,這樣在邏輯上可以很快梳理,其實本質上還是使用了角色的從屬關系。
比如,版主 A 是攝影器材板塊的版主,而版主 B 是手機平板板塊的版主,現在新開放了一個電子數碼版塊,而需求規定 A 和 B 都同時具備管理電子數碼板塊的權限,但是 A 不具備管理手機平板版塊的權限,反之亦然,那么就需要設置一個電子數碼板塊的版主角色(中間角色),同時讓 A 和 B 擁有該角色即可。
~~~
//新建攝影器材版主角色
var photographicRole = new AV.Role('Photographic');
//新建手機平板版主角色
var mobileRole=new AV.Role('Mobile');
//新建電子數碼版主角色
var digitalRole=new AV.Role('Digital');
AV.Promise.all([
// 先行保存 photographicRole 和 mobileRole
photographicRole.save(),
mobileRole.save(),
]).then(function([r1, r2]) {
// 將 photographicRole 和 mobileRole 設為 digitalRole 一個子角色
digitalRole.getRoles().add(photographicRole);
digitalRole.getRoles().add(mobileRole);
digitalRole.save();
// 新建一個帖子對象
var Post = AV.Object.extend('Post');
// 新建攝影器材板塊的帖子
var photographicPost = new Post();
photographicPost.set('title', '我是攝影器材板塊的帖子!');
// 新建手機平板板塊的帖子
var mobilePost = new Post();
mobilePost.set('title', '我是手機平板板塊的帖子!');
// 新建電子數碼板塊的帖子
var digitalPost = new Post();
digitalPost.set('title', '我是電子數碼板塊的帖子!');
// 新建一個攝影器材版主可寫的 ACL 實例
var photographicACL = new AV.ACL();
photographicACL.setPublicReadAccess(true);
photographicACL.setRoleWriteAccess(photographicRole,true);
// 新建一個手機平板版主可寫的 ACL 實例
var mobileACL = new AV.ACL();
mobileACL.setPublicReadAccess(true);
mobileACL.setRoleWriteAccess(mobileRole,true);
// 新建一個手機平板版主可寫的 ACL 實例
var digitalACL = new AV.ACL();
digitalACL.setPublicReadAccess(true);
digitalACL.setRoleWriteAccess(digitalRole,true);
// photographicPost 只有 photographicRole 可以讀寫
// mobilePost 只有 mobileRole 可以讀寫
// 而 photographicRole,mobileRole,digitalRole 均可以對 digitalPost 進行讀寫
photographicPost.setACL(photographicACL);
mobilePost.setACL(mobileACL);
digitalPost.setACL(digitalACL);
return AV.Promise.all([
photographicPost.save(),
mobilePost.save(),
digitalPost.save(),
]);
}).then(function([r1, r2, r3]) {
// 保存成功
}, function(errors) {
// 保存失敗
});;
~~~
### [權限的分級](#權限的分級)
在日常生活中,我們也遇到過權限分級的問題,例如新的系統又一次升級,引入了超級管理員的概念,這個超級管理員可以對系統里面任何的對象進行「讀寫操作」,按照前文的做法,需要用代碼實現如下步驟:
1. 創建超級管理員角色
2. 遍歷云端現有的帖子,為所有對象添加超級管理員的「ACL 讀寫權限」
3. 保存數據
開發者一旦面對這種邏輯重復的代碼,一般都會想到:有沒有一個快捷的操作可以一次性為某一個角色添加整個 Class 添加權限?
當然,LeanCloud 也已經提供了便捷的可視化的操作。打開?控制臺?>?存儲?>?Post?>?其他?>?權限設置,如下圖所示:

* 在這里你可以設置某一個角色對?`Post`?的操作權限:選擇?指定用戶,在指定角色中輸入?`superAdmin`,并且點擊添加即可。
如此設置,無需書寫代碼,在項目迭代過程中有角色加入都可以用這種便捷的方式進行操作。
權限設置的參數以及操作解釋如下:
| 參數名 | 含義 |
| --- | --- |
| add_fields | 添加新字段到 class |
| create | 保存一個從未創建過的新對象 |
| delete | 刪除一個對象 |
| find | 發起一次對象列表查詢 |
| get | 通過 objectId 獲取對象 |
| update | 保存一個已經存在并且被修改的對象 |
| 權限對象名 | 含義 |
| --- | --- |
| 所有用戶 | 任意用戶均有權限 |
| 登錄用戶 | 用戶登錄之后,具有權限 |
| 指定用戶 | 可以指定具體的用戶(`AVUser`)也可以指定具體的角色(`AVRole`) |
## [超級權限](#超級權限)
ACL 可以滿足常見的需求,但是?`_User`?表比較特殊,它會忽略 ACL 的設置,表現為:任何用戶都無法修改其他用戶的屬性,比如當前登錄的用戶是 A,而他想通過請求去修改 B 用戶的用戶名,密碼或者其他自定義屬性,是不會生效的。
但是有時一些應用的需求較為特殊,比如,論壇的管理員可以修改某些用戶的昵稱,性別(假設昵稱和性別是存儲在?`_User`?的?`gender`,`age`?的字段上),此時通過設置管理員擁有該用戶的 ACL 寫的權限是無法實現預想效果的。因此,我們提供了一種方式去提高操作的權限,在 SDK 中的實現方式是在初始化的時候,增加一個 Master Key 的字段作為提高權限的接口。Master Key 可以在?`控制臺`?->?`設置`?->?`應用 Key`?中獲取。
在 Node.js 運行時中可以使用如下代碼初始化 SDK:
~~~
AV.init({
appId: APP_ID,
appKey: APP_KEY,
masterKey: MASTER_KEY,
})
AV.Cloud.useMasterKey();
~~~
## [最佳實踐](#最佳實踐)
本章節的目的是介紹如何在?[云引擎](https://leancloud.cn/docs/acl_guide_leanengine.html)?里面使用 ACL 。
我們先來探索一個需求:某個應用它擁有眾多客戶端,iOS、Andorid、Windows 等,還有 Web 版,未來可能還會有手環,手表客戶端,那么關于 ACL 的代碼會遍布在所有客戶端,那么開發者就會需要不斷的升級和維護邏輯十分類似的客戶端代碼,到了這一步,我們更推薦,開發者在服務端定義一段 ACL 的邏輯,統一處理 ACL 權限分配的問題。
【總結】假如應用的平臺比較固定,那么就可以考慮采取本文前面所介紹的客戶端代碼實現 ACL 代碼。如果應用的平臺比較多,多平臺多客戶端就推薦使用?[云引擎 Hook 函數使用 ACL](https://leancloud.cn/docs/acl_guide_leanengine.html)。