[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 > 其他 > 权限设置,如下图所示:
![acl_in_console](http://ac-lhzo7z96.clouddn.com/1442281757400)
* 在这里你可以设置某一个角色对 `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)。