程序代码中的一个逻辑 bug 可能会引发数据错误、界面显示错乱,甚至是程序崩溃。作为开发者,谁摊上这事都恨不能赶紧修掉 bug,万不可拖到使用者们义愤填膺地来砸招牌。
可如果是一个数据安全性的问题呢?使用者还是能正常使用程序,没人知道这个问题的存在或隐患,包括开发者自己。直到某天使用者们突然收到很多骚扰邮件,他们在应用中的数据被人恶意篡改,甚至所绑定的信用卡信息也被窃取……此刻,开发者才幡然悔悟——数据安全性问题可不像之前提到的那类 bug 那样容易搞定,即使堵上了漏洞,使用者的信息也还是泄露了出去,被篡改的信息没那么容易恢复,经济上的损失更是难以估算。
LeanCloud 作为一个将各项服务的 API 直接开放给客户端,并拥有数以万计开发者和海量应用的 BaaS 服务,势必要提供针对数据安全的各种保障机制。但更重要的是,只有开发者们了解并掌握了这些数据安全机制的用法,才能根据实际需求来有效地为应用数据加上防护措施。
访问 LeanCloud 的 API 需要提供应用的 AppID 和 AppKey,或是 AppID 和 MasterKey。
当客户端需要直接访问 LeanCloud 提供的 API 时,开发者必须将 AppKey 编码到客户端的代码中。这样用户只要通过反编译客户端代码或者截获客户端发往服务器的请求,即可轻易获取 AppKey。
因此千万不要认为:
AppKey 是对数据的一种保护措施,只要不主动泄漏,就没人能获得 AppKey。
而是要明白:
AppKey 是公开的,需要结合 LeanCloud 提供的其他安全措施来保障应用数据的安全性!
使用 MasterKey 来访问 LeanCloud API 会跳过所设置的任何安全措施,方便对数据进行强制性的修改。因此请尽量保证 MasterKey 不会被泄漏,也不要将 MasterKey 放在任何客户端代码中,它只可出现在你的后端服务器或者云引擎的代码中。
首先我们要理解要用到的专有名词:Class 对应传统数据库中的表(Table),Object 对应表中的行记录(Row/Record),field 对应为列或字段。LeanCloud 可以分别给每个 Class、每个 Object 和每个 field 设置不同的访问权限,保证每位用户只能访问到被授权的数据。
Class 权限只可以在控制台中进行设置,顾名思义,对应的是整个 Class 的读写权限。下图为 Class 的权限设置页面:
可以控制的权限有五种:
add_fields
保存 Object 时,如果对应的字段不存在,是否允许自动创建新的字段。如果已经在控制台创建好 Class 的所有字段,最好对任意用户都关闭此权限,防止被写入一些脏数据,占用存储空间,同时拖慢数据传输速度。另外在控制台里也可以关闭整个应用的「创建 Class」的权限,作用类似。
create
是否允许创建新的 Object。对于需要登录用户或者拥有指定授权的用户才能创建内容的场景,可以考虑根据情况设置此权限。
delete
是否允许删除 Object。
find
是否允许根据条件对 Object 进行查询。对于一些不想让其他用户可以批量获取的 Class,可以通过此权限进行限制,这样用户只能通过 objectId 进行获取。因为 objectId 默认不是自增的形式,因此如果没有此权限的话,很难通过遍历 objectId 的方法进行批量获取。
get
是否允许直接通过 objectId 直接获取 Object。可以配合 find
权限使用。
update
是否允许更新 Object 内容。
可以针对以下 4 种维度的用户分类来指定权限:
所有用户
所有使用 AppKey 来访问 API 的请求,都可以进行此项权限对应的操作。
登录用户
使用了当前应用下一个合法的 _User 表中的 Object 所对应的 sessionToken 进行访问的用户,才可以进行此项权限对应的操作。此限制适合在只有登录之后才允许发布和阅读内容的场景下使用。
指定用户
必须使用某个指定的 _User 表对应的 sessionToken 进行访问的用户,才能进行此项权限对应的操作。灵活性较大,可以适应各个场景。
指定角色
必须使用某个指定的 _Role 表中(一系列 _User 的合集)对应的 sessionToken 进行访问的用户,才能进行此项权限对应的操作。例如在论坛中需要给各个版块的版主特殊权限,并且版主可能会经常变更,这样使用角色来管理权限会比较方便。
除了以上权限之外,每一个 field 也有特殊的权限可以设置:
只读
只允许客户端读取数据,不允许写入。
客户端不可见
有时一个 Object 上需要保存一些关联的数据,但是不想要将此数据展示给用户,一种方案是通过创建一个新的 Class 来保存这一数据,然后限制客户端读取这个 Class 来解决,另一种方案就是直接使用此选项来解决。
除了上面提到的 Class 权限,往往还需要针对具体的 Object 来设置不同的权限。
在 LeanCloud 的数据存储服务中,每个 Object 都会有一列特殊的属性 ACL (Access Control List) 来决定一个 Object 的实际读写权限。如果需要针对每个 Object 设置不同的读写权限,比如只允许作者修改自己发布过的文章,可以直接通过设置对应的 ACL 来实现。
可以设置在创建一个 Class 时新增加的 Object 的默认 ACL 值:
ACL 的默认值只覆盖了 4 个常用场景,如果有特殊需求,在创建好 Class 之后直接修改其中 ACL 字段的默认值即可。这与为一个普通的 field 设置默认值的方法相同:
ACL 字段的值是一个 JSON 对象,最外层的 key 的值可能有如下情况:
*
所有人
_User 用户表中的某个 objectId
一个指定用户
role:{{roleName}}
_Role 表中的 name 与 {{roleName}}
相匹配的角色所对应的所有用户,如 role:authors
。
_owner
这是个特殊的值,在新对象创建后,它会自动变成发出创建此对象请求的用户的 objectId。
也可以在每个 Object 上设置具体的 ACL:
当然,在控制台里给每个 Object 分别设置权限并不现实,最好的方式是通过上面提到的设置 ACL 字段的默认值,来确保所有新创建的 Object 都设置了合适的 ACL 值。
如果有通过默认 ACL 值依然不能满足的情况,则可以考虑在插入数据时同时设置 ACL 权限:
$ curl -X POST
-H "X-LC-Id: XEComQ0KWPD7r3RDQYrSCaPV-gzGzoHsz"
-H "X-LC-Key: WFzTQJdI39iR6JJO9ytR290i"
-H "Content-Type: application/json"
-d '{"content": "Hello, world!","ACL": {"*": {"read": true, "write": false}, "58ce7493ac502e00589e3105": {"write": true}}}'
https://api.leancloud.cn/1.1/classes/Post
除了直接使用 REST API 创建数据之外,其他各个语言的 SDK 也都提供了相应的方法来在创建对象时设置 ACL。
另外需要注意,如果一条数据的读写操作对应的 Object 与 Class 都设置了对应的权限,这时需要两个条件都满足,才能成功进行这次操作。而对于同一个请求,可能会有多个 ACL 设置,这时只要满足其中一项规则就可以执行这次操作了。比如不允许任何人( *
)写入操作,但是允许特定的用户写入,这时这个用户是可以执行写入操作的。
上面提到的一些安全措施已经能够覆盖到大部分场景了,但是依然会有一些场景不能简单地通过设置权限来解决。典型的场景有:需要对非用户的维度进行权限或数据合法性校验。比如论坛,不允许在已经关闭的帖子下添加新的回复,似乎使用上面提到过的机制很难做到这个限制。
这个时候就需要引入云引擎来完成需求了。云引擎是 LeanCloud 提供的另一项服务,在之上除了像传统提供虚拟机的云服务一样,搭建自己的基于 HTTP 的网站服务之外,还可以结合数据存储服务,在读写数据时进行校验。
云引擎在进行数据读写时默认会使用 MasterKey 来进行鉴权,因此会拥有最高权限。不过此时因为代码运行在服务器上,不会被第三者篡改,因此只要合理使用,开发者不必担心安全性问题。
回到上面的问题,如何限制用户在已经关闭的帖子下,创建新的回复呢?可以考虑使用云引擎的 beforeSave
这个 hook 来进行检查:
@engine.before_save('Comment')
def before_comment_save(comment):
post = comment.get('post')
post.fetch() # post 是个 Pointer,因此需要先调用 `fetch()` 来获取数据
if post.get('isClosed'):
raise LeanEngineError('post is closed.')
# 除此之外,还可以对 Comment 做一些其他检查,比如是否有禁止出现的词汇
将这段代码部署在云引擎中,这样所有请求在已经关闭的帖子下创建回复的请求时都会被拒绝掉。
除了 beforeSave
hook 之外,还有 beforeUpdate
、 beforeDelete
等等其他种类的 hook 来在对应的时机下执行,具体可参考 文档。上述代码是使用 Python 来实现的,LeanCloud 还支持使用 Node.js、PHP、Java 来编写云函数。
再设想一种场景,假设我们在 Post 表中使用一个叫做 commentsCount 的字段来保存帖子的回复数量,用于客户端展示。当然这个字段里的数据属于冗余,是可以直接通过查询 Comment 表中对应的数量来获取的,但是这就意味着在客户端中要展示 100 个帖子的列表时,需要再进行 100 次 API 调用来查询回复数量,并且底层数据库也会有响应的查询耗时,因此为了提升应用响应性能,一般情况下开发者都会设置这个冗余字段。
但是这时候问题来了,我们如何确保这个字段的值是正确的呢?当然可以让客户端每次发送完创建评论的请求后,再次发送一次请求去更新这个 commentsCount 字段。但是对于直接绕过了客户端、直接通过 curl 来发帖的用户,他们可能不会遵守「发完贴要更新回复数量」这个义务,甚至有人会直接设置一个错误的值进去,破坏应用数据的完整性,或带来其他影响,这时依然可以通过云引擎的 afterSave
hook 来完成任务:
@engine.after_save('Comment')
def after_comment_save(comment):
post = comment.get('post')
post.increment('commentsCount', 1)
post.save()
除了将此段代码部署在云引擎,还需要结合上面的介绍,将 Post 表中的 commentsCount 设置为「只读」,这样就不用担心有人会忘记更新评论数,以及故意破坏数据完整性的问题了。
此外,有时我们还需要做更加复杂的权限设置,比如在用户查询数据时要过滤掉某些条件(只允许查询最近一个月创建的帖子),或者对相同 IP 发送的请求做频率限制,或者让某个没有权限的用户临时写入或查询某些数据等等。
这时可以通过云函数进行这种操作:
@engine.define('godApi')
def god_api(**params):
do_what_you_want_with_input_data(params)
return what_you_want()
事实上,将对应的数据权限全部关闭,只通过云函数来操作数据,使用 LeanCloud 进行应用开发,与自己搭建服务器来编写接口具有同样的数据安全性保障。
上面介绍了基于 LeanCloud 开发应用、保障数据安全的一系列措施,灵活性从弱到强。实际开发过程中,开发者可以根据具体情况,选择相应的机制。
比如只对单一数据条目进行增删改查,并且只针对用户进行权限校验,完全可以利用 Class / Object 权限设置来保障数据的安全性与统一性。除此之外的情况,则可以考虑将对应 Class 的读或写权限关闭,利用云引擎来进行安全性检查和数据校验。