codecamp

Moralis 安全

客户端与服务器

当应用程序首次连接到 Moralis 时,它会使用应用程序 ID 来标识自己。此密钥作为您的应用程序的一部分提供,任何人都可以从他们的设备反编译您的应用程序或代理网络流量以找到它。使用 JavaScript 可以更轻松地利用这一漏洞——只需在浏览器中“查看源代码”并立即找到您的客户端密钥。

这就是 Moralis 具有许多其他安全功能来帮助您保护数据的原因。

客户端密钥已提供给您的用户,因此任何仅使用客户端密钥即可完成的事情对公众来说是可行的,甚至是恶意黑客。

另一方面,万能钥匙绝对是一种安全机制。使用主密钥可以绕过应用程序的所有安全机制,例如类级别权限和 ACL。拥有主密钥就像拥有对应用程序服务器的 root 访问权限,您应该像保护生产机器的 root 密码一样热情地保护您的主密钥。

总体理念是限制客户端的权力(使用客户端密钥)并执行任何需要 Cloud Code 中的主密钥的敏感操作。您将在标题为“在云代码中实现业务逻辑”的部分中学习如何最好地利用这种能力。

最后说明:建议在您的服务器中设置 HTTPS 和 SSL,以避免中间人攻击,但 Moralis 也适用于非 HTTPS 连接。

类级权限

第二级安全性是架构和数据级别。 在此级别执行安全措施将限制客户端应用程序访问和创建 Moralis 数据的方式和时间。 当您第一次开始开发 Moralis 应用程序时,会设置所有默认值,以便您可以成为更有效率的开发人员。 例如:

  • 客户端应用程序可以在 Moralis 上创建新类。
  • 客户端应用程序可以向类添加字段。
  • 客户端应用程序可以修改或查询 Moralis 上的对象。

您可以将这些权限中的任何一个配置为适用于所有人、任何人或应用程序中的特定用户或角色。

角色是包含用户或其他角色的组,您可以将其分配给对象以限制其使用。 授予角色的任何权限也授予其任何子项,无论他们是用户还是其他角色,使您能够为您的应用程序创建访问层次结构。

一旦你确信你在你的应用程序中拥有正确的类和类之间的关系,你应该开始通过执行以下操作来锁定它:

几乎您创建的每个类都应该在某种程度上调整这些权限。 对于每个对象都具有相同权限的类,类级别的设置将是最有效的。 例如,一个常见的用例需要拥有一类任何人都可以读取但没有人可以写入的静态数据。

配置类级别权限

Moralis 允许您指定每个类允许的操作。 这使您可以限制客户端访问或修改您的类的方式。 要更改这些设置,请转到“数据浏览器”,选择一个类,然后单击“安全”按钮。

您可以配置客户端对所选类执行以下每个操作的能力:

  • 读:
    • Get​:如果用户知道自己的​objectIds​,就可以获取该表中的对象。
    • Find​:任何拥有 Find 权限的人都可以查询表中的所有对象,即使他们不知道自己的 ​objectId​。除非您在每个对象上放置 ACL,否则任何具有公共 Find 权限的表都将完全被公众读取。
  • 写:
    • 更新:任何拥有更新权限的人都可以修改表中没有 ACL 的任何对象的字段。对于公开可读的数据,例如游戏关卡或资产,您应该禁用此权限。
    • 创建:与更新一样,任何拥有创建权限的人都可以创建类的新对象。与更新权限一样,您可能希望将其关闭以获取公开可读的数据。
    • 删除:有了这个权限,人们可以删除表中没有 ACL 的任何对象。他们只需要它的​objectId​。
  • 添加字段:Moralis 类具有在创建对象时推断的模式。在开发应用程序时,这很棒,因为您可以向对象添加新字段,而无需在后端进行任何更改。但是一旦你发布了你的应用程序,就很少需要自动将新字段添加到你的类中。当您向公众提交您的应用程序时,您几乎应该始终关闭所有课程的此权限。

对于上述每个操作,您可以向所有用户授予权限(这是默认设置),或将权限锁定到角色和用户列表。

例如,一个应该对所有用户都可用的类将通过仅启用 get 和 find 设置为只读。 通过只允许创建,可以将日志记录类设置为只写。 您可以通过提供对一组特定用户或角色的更新和删除访问权限来启用用户生成内容的审核。

对象级访问控制

锁定架构和类级别权限后,就该考虑用户如何访问数据了。 对象级访问控制使一个用户的数据与另一个用户的数据分开,因为有时一个类中的不同对象需要由不同的人访问。 例如,用户的私人个人数据应该只有他们才能访问。

对于那些想要存储和保护用户特定数据而不需要显式登录的应用程序,Moralis 还支持匿名用户的概念。

当用户登录应用程序时,他们会启动与 Moralis 的会话。 通过此会话,他们可以添加和修改自己的数据,但无法修改其他用户的数据。

访问控制列表

控制谁可以访问哪些数据的最简单方法是通过访问控制列表,通常称为 ACL。 ACL 背后的想法是每个对象都有一个用户和角色列表以及该用户或角色拥有的权限。 用户需要读取权限(或必须属于具有读取权限的角色)才能检索对象的数据,并且用户需要写入权限(或必须属于具有写入权限的角色)才能更新或删除该对象 目的。

拥有用户后,您就可以开始使用 ACL。 请记住,可以通过传统的用户名/密码注册、通过 Facebook 或 Twitter 等第三方登录系统,甚至使用 Moralis 的自动匿名用户功能来创建用户。 要将当前用户数据的 ACL 设置为不公开可读,您所要做的就是:

var user = Moralis.User.current();
user.setACL(new Moralis.ACL(user));

大多数应用程序都应该这样做。如果您存储任何敏感的用户数据,例如电子邮件地址或电话号码,则需要设置这样的 ACL,以便其他用户看不到用户的私人信息。如果一个对象没有 ACL,那么每个人都可以读写。默认情况下,Moralis 在新的用户对象上设置 ACL,以便只有用户可以读取或写入他们自己的数据。 (如果您作为开发人员需要更新其他 _User 对象,请记住您的主密钥可以提供执行此操作的能力。)

如果您希望用户拥有一些公开的数据和一些私有的数据,最好有两个单独的对象。您可以从公共数据中添加指向私有数据的指针。

您还可以为类中的不同列设置不同的权限。有关实际演示,请参阅视频。

当然,您可以对一个对象设置不同的读写权限。例如,这是为用户的公开帖子创建 ACL 的方式,任何人都可以阅读它:

var acl = new Moralis.ACL();
acl.setPublicReadAccess(true);
acl.setWriteAccess(Moralis.User.current().id, true);

有时在每个用户的基础上管理权限是不方便的,并且您希望拥有一组获得相同待遇的用户(例如一组具有特殊权力的管理员)。 角色是一种特殊的对象,可让您创建一组可以全部分配给 ACL 的用户。 角色的最佳之处在于,您可以在角色中添加和删除用户,而无需更新仅限于该角色的每个对象。 要创建只能由管理员写入的对象:

var acl = new Moralis.ACL();
acl.setPublicReadAccess(true);
acl.setRoleWriteAccess("admins", true);

当然,这个片段假设你已经创建了一个名为“admins”的角色。 当您在开发应用程序时设置了一小组特殊角色时,这通常是合理的。 角色也可以即时创建和更新——例如,在每次连接后将新朋友添加到“friendOf___”角色。

这一切只是一个开始。 应用程序可以通过 ACL 和类级别权限强制执行各种复杂的访问模式。 例如:

  • 对于私有数据,可以将读写权限限制为所有者。
  • 对于留言板上的帖子,“版主”角色的作者和成员可以拥有“写入”权限,而普通大众可以拥有“阅读”权限。
  • 对于只能由开发人员使用主密钥通过 REST API 访问的日志记录数据,ACL 可以拒绝所有权限。
  • 由特权用户组或开发人员创建的数据(例如当前的全局消息)可以具有公共读取权限,但将写入权限限制为“管理员”角色。
  • 从一个用户发送给另一个用户的消息可以只为这些用户提供“读取”和“写入”访问权限。

这是一个 ACL 的格式,它限制所有者(其 objectId 由“​aSaMpLeUsErId​”标识)的读写权限并允许其他用户读取该对象:

{
    "*": { "read":true },
    "aSaMpLeUsErId": { "read" :true, "write": true }
}

这是使用角色的 ACL 格式的另一个示例:

{
    "role:RoleName": { "read": true },
    "aSaMpLeUsErId": { "read": true, "write": true }
}

指针权限

指针权限是一种特殊类型的类级别权限,它根据存储在这些对象的指针字段中的用户,在类中的每个对象上创建一个虚拟 ACL。例如,给定一个具有 owner 字段的类,在 owner 上设置读取指针权限将使类中的每个对象只能由该对象的 owner 字段中的用户读取。对于有发送者和接收者字段的类,接收者字段的读指针权限和发送者字段的读写指针权限将使类中的每个对象在发送者和接收者字段中用户可读,可写仅由用户在发件人字段中。

鉴于对象通常已经具有指向应该对对象具有权限的用户的指针,指针权限提供了一种简单而快速的解决方案,用于使用已经存在的数据保护您的应用程序,无需编写任何客户端代码或云代码。

指针权限类似于虚拟 ACL。它们不会出现在 ACL 列中,但如果您熟悉 ACL 的工作原理,则可以将它们视为 ACL。在上面的发送方和接收方示例中,每个对象的行为就好像它具有以下 ACL:

{
    "<SENDER_USER_ID>": {
        "read": true,
        "write": true
    },
    "<RECEIVER_USER_ID>": {
        "read": true
    }
}

请注意,此 ACL 实际上并不是在每个对象上创建的。 当您添加或删除指针权限时,任何现有的 ACL 都不会被修改,并且任何尝试与对象交互的用户只有在指针权限创建的虚拟 ACL 和对象上已经存在的真实 ACL 都允许的情况下才能与对象交互 互动。 出于这个原因,将指针权限和 ACL 结合起来有时会让人感到困惑,因此我们建议对没有设置很多 ACL 的类使用指针权限。 幸运的是,如果您以后决定使用云代码或 ACL 来保护您的应用程序,则很容易删除指针权限。

需要身份验证权限

CLP ​requiresAuthentication ​防止任何未经身份验证的用户执行受 CLP 保护的操作。

例如,如果您想允许经过身份验证的用户从您的应用程序中查找并获取公告,并且您的管理员角色拥有所有特权,您可以设置 CLP:

// POST https://my-moralis-dapp.com/schemas/Announcement
// Note: You need to use PUT http method if the class already exists
// Set the X-Parse-Application-Id and X-Parse-Master-Key header
// body:
{
  "classLevelPermissions":
  {
    "find": {
      "requiresAuthentication": true,
      "role:admin": true
    },
    "get": {
      "requiresAuthentication": true,
      "role:admin": true
    },
    "create": { "role:admin": true },
    "update": { "role:admin": true },
    "delete": { "role:admin": true }
  }
}

影响:

  • 未经身份验证的用户将无法执行任何操作。
  • 经过身份验证的用户(任何具有有效 ​sessionToken ​的用户)将能够读取该类中的所有对象。
  • 属于管理员角色的用户将能够执行所有操作。

警告:请注意,这绝不会保护您的内容,如果您允许任何人登录到您的服务器,每个客户端仍然可以查询此对象。

CLP 和 ACL 交互

类级别权限 (CLP) 和访问控制列表 (ACL) 都是保护您的应用程序的强大工具,但它们的交互并不总是与您预期的完全一致。 它们实际上代表了两个独立的安全层,每个请求都必须通过这些层才能返回正确的信息或进行预期的更改。 这些层,一个在类级别,一个在对象级别,如下所示。 请求必须通过两层检查才能获得授权。 请注意,尽管行为类似于 ACL,但指针权限是一种类级别权限,因此请求必须通过指针权限检查才能通过 CLP 检查。

clp_vs_acl_diagram

如您所见,当您同时使用 CLP 和 ACL 时,用户是否有权发出请求会变得很复杂。 让我们看一个示例,以更好地了解 CLP 和 ACL 如何交互。 假设我们有一个 Photo 类,其中包含一个对象 photoObject。 我们的应用中有 2 个用户,user1 和 user2。 现在假设我们在 Photo 类上设置了 Get CLP,禁用公共 Get,但允许 user1 执行 Get。 现在让我们在 photoObject 上设置一个 ACL 以允许读取 - 包括 GET - 仅用于 user2。

您可能期望这将允许 user1 和 user2 都获取 photoObject,但是由于身份验证的 CLP 层和 ACL 层始终有效,因此实际上它使 user1 和 user2 都无法获取 photoObject。如果 user1 尝试获取 photoObject,它会通过 CLP 层的认证,但会因为没有通过 ACL 层而被拒绝。同理,如果 user2 试图获取 photoObject,也会在 CLP 认证层被拒绝。

现在让我们看一个使用指针权限的示例。假设我们有一个带有对象 myPost 的 Post 类。我们的应用中有两个用户,poster 和 viewer。假设我们添加了指针权限,允许 Post 类的 Creator 字段中的任何人对该对象进行读写访问,而对于 myPost 对象,poster 是该字段中的用户。对象上还有一个 ACL,为查看器提供读取访问权限。您可能期望这将允许发布者阅读和编辑 myPost,并且查看者可以阅读它,但是查看者将被指针权限拒绝,并且发布者将被 ACL 拒绝,所以同样,两个用户都无法访问目的。

由于 CLP、指针权限和 ACL 之间的复杂交互,我们建议在一起使用它们时要小心。仅使用 CLP 来禁用特定请求类型的所有权限,然后将指针权限或 ACL 用于其他请求类型通常很有用。例如,您可能希望为 Photo 类禁用 Delete,但随后在 Photo 上放置一个指针权限,以便创建它的用户可以编辑它,而不是删除它。由于指针权限和 ACL 交互的方式特别复杂,我们通常建议只使用这两种安全机制中的一种。

安全边缘案例

Moralis 中有一些特殊的类不遵循与其他所有类相同的所有安全规则。 并非所有类都完全遵循类级别权限 (CLP) 或访问控制列表 (ACL) 的定义方式,这里记录了这些例外情况。

这里,“正常行为”是指 CLP 和 ACL 正常工作,而其他特殊行为在脚注中描述。

   ​_User  ​_Installation
 Get  normal behavior [1, 2, 3]  ignores CLP, but not ACL
 Find  normal behavior [3]  master key only [6]
 Create  normal behavior [4]  ignores CLP
 Update  normal behavior [5]  ignores CLP, but not ACL [7]
 Delete  normal behavior [5]  master key only [7]
 Add Field  normal behavior  normal behavior

  1. 登录或 REST API 中的 ​/server/login​ 不尊重用户类上的 Get CLP。登录仅基于用户名和密码,不能使用 CLP 禁用。
  2. 检索当前用户,或基于会话令牌成为用户,这在 REST API 中都是 ​/server/users/me​,不尊重用户类上的 Get CLP。
  3. 读取 ACL 不适用于登录用户。例如,如果所有用户的 ACL 都禁用了读取,那么对用户执行查找查询仍将返回登录用户。但是,如果禁用 Find CLP,则尝试对用户执行查找仍将返回错误。
  4. 创建 CLP 也适用于注册。因此,在用户类上禁用 Create CLPs 也会禁止人们在没有主密钥的情况下进行注册。
  5. 用户只能更新和删除自己。更新和删除的公共 CLP 可能仍然适用。例如,如果您禁用用户类的公共更新,则用户无法编辑自己。但无论用户的写入 ACL 是什么,该用户仍然可以更新或删除自己,其他用户不能更新或删除该用户。然而,与往常一样,使用主密钥允许用户更新其他用户,而与 CLP 或 ACL 无关。
  6. 获取安装请求通常遵循 ACL。除非您提供 ​installationId ​作为约束,否则不允许查找没有主密钥的请求。

云代码中的数据完整性

对于大多数应用程序,您只需要关心密钥、类级权限和对象级 ACL 即可确保您的应用程序和用户数据的安全。但是,有时,您会遇到它们还不够的边缘情况。对于其他一切,有 Cloud Code。

Cloud Code 允许您将 JavaScript 上传到 Moralis 的服务器,我们将为您运行它。与在用户设备上运行的可能已被篡改的客户端代码不同,Cloud Code 保证是您编写的代码,因此可以承担更多责任。

Cloud Code 的一个特别常见的用例是防止存储无效数据。对于这种情况,恶意客户端不能绕过验证逻辑尤为重要。

要创建验证函数,Cloud Code 允许您为您的类实现 beforeSave 触发器。每当保存对象时都会运行这些触发器,并允许您修改对象或完全拒绝保存。例如,这是您创建 Cloud Code beforeSave 触发器以确保每个用户都设置了电子邮件地址的方式:

Moralis.Cloud.beforeSave('_User', request => {
  const user = request.object;
  if (!user.get("email")) {
    throw "Every user must have an email address.";
  }
});

验证可以锁定您的应用程序,以便只接受某些值。 您还可以使用 afterSave 验证来规范化您的数据(例如,以相同的方式格式化所有电话号码或货币)。 您可以保留直接从客户端应用程序访问 Moralis 数据的大部分生产力优势,但您也可以动态为您的数据强制执行某些不变量。

需要验证的常见场景包括:

  • 确保电话号码的格式正确。
  • 清理数据,使其格式标准化。
  • 确保电子邮件地址看起来像真实的电子邮件地址。
  • 要求每个用户指定特定范围内的年龄。
  • 不允许用户直接更改计算字段。
  • 除非满足某些条件,否则不允许用户删除特定对象。

在云代码中实现业务逻辑

虽然验证在 Cloud Code 中通常很有意义,但某些操作可能特别敏感,应尽可能小心谨慎。在这些情况下,您可以完全删除客户端的权限或逻辑,而是将所有此类操作集中到 Cloud Code 函数中。

当调用云代码函数时,它可以使用可选的 {​useMasterKey:true​} 参数来获得修改用户数据的能力。使用主密钥,您的 Cloud Code 函数可以覆盖任何 ACL 并写入数据。这意味着它将绕过您在前面部分中设置的所有安全机制。

假设您希望允许用户“喜欢”一个 Post 对象,而不给予他们对该对象的完全写入权限。您可以通过让客户端调用 Cloud Code 函数而不是修改 Post 本身来做到这一点:

应谨慎使用万能钥匙。仅在需要安全覆盖的单个 API 函数调用中将 ​useMasterKey ​设置为 ​true​:

Moralis.Cloud.define("like", async request => {
  var post = new Moralis.Object("Post");
  post.id = request.params.postId;
  post.increment("likes");
  await post.save(null, { useMasterKey: true })
});

Cloud Code 的一个非常常见的用例是向特定用户发送推送通知。 一般来说,不能信任客户端直接发送推送通知,因为他们可以修改警报文本,或推送给他们不应该发送的人。 您的应用程序设置将允许您设置是否启用“客户端推送”; 我们建议您确保它已被禁用。 相反,您应该编写 Cloud Code 函数,在发送推送之前验证要推送和发送的数据。

客户端类创建

默认情况下,Moralis 允许任何 SDK 用户通过创建新类和更改现有类的结构来修改数据库。

这在开发阶段非常有用,但在您投入生产时应该关闭,以保护您的数据库免受垃圾邮件的影响(以防有人使用 SDK 用新的类填充您的数据库或向现有列添加大量列)。

这可以在服务器设置中完成。

Screenshot 2021-11-02 at 19

总结

Moralis 为您提供了多种方法来保护应用程序中的数据。在构建应用程序并评估要存储的数据类型时,您可以决定选择哪种实现方式。

您应用程序中的大多数类都属于几个易于保护的类别之一。对于完全公开的数据,您可以使用类级别的权限来锁定表,使其不受任何人的公开可读和可写。对于完全私有的数据,您可以使用 ACL 来确保只有拥有数据的用户才能读取它。但有时,您会遇到不想要完全公开或完全私有的数据的情况。例如,您可能有一个社交应用程序,其中您有一个用户的数据,这些数据应该仅供他们认可的朋友阅读。为此,您需要结合本指南中讨论的技术来准确启用您想要的共享规则。

我们希望您能使用这些工具尽一切可能保护您的应用数据和用户数据的安全。一起,我们可以让网络成为一个更安全的地方。


Moralis 角色
Moralis 数据
温馨提示
下载编程狮App,免费阅读超1000+编程语言教程
取消
确定
目录

Moralis 文件

Moralis 工具

关闭

MIP.setData({ 'pageTheme' : getCookie('pageTheme') || {'day':true, 'night':false}, 'pageFontSize' : getCookie('pageFontSize') || 20 }); MIP.watch('pageTheme', function(newValue){ setCookie('pageTheme', JSON.stringify(newValue)) }); MIP.watch('pageFontSize', function(newValue){ setCookie('pageFontSize', newValue) }); function setCookie(name, value){ var days = 1; var exp = new Date(); exp.setTime(exp.getTime() + days*24*60*60*1000); document.cookie = name + '=' + value + ';expires=' + exp.toUTCString(); } function getCookie(name){ var reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)'); return document.cookie.match(reg) ? JSON.parse(document.cookie.match(reg)[2]) : null; }