控制对特定字段的访问权限

本页面以构建安全规则编写安全规则的条件中的概念为基础,介绍了如何使用 Cloud Firestore Security Rules 来创建规则,以允许客户端对文档中的某些字段执行操作,而不允许对其他字段执行操作。

有时候,您可能想要控制在字段级别的文档更改而不是文档级别的更改。

例如,您可能想要允许客户端创建或更改文档,但不允许他们修改该文档中的特定字段。或者,您可能希望要求某个客户端创建的所有文档都包含一组特定字段。本指南介绍如何使用 Cloud Firestore Security Rules 完成其中的一部分任务。

仅允许读取特定字段

Cloud Firestore 中的读取操作在文档级执行。用户要么检索到整份文档,要么什么都检索不到,而无法只检索到文档的部分内容。想要只使用安全规则便可阻止用户读取文档中的特定字段是不可能的。

如果您想要对某些用户隐藏文档中的特定字段,较好的方法是将它们放在单独的文档中。例如,您不妨考虑在 private 子集合中创建这样的文档:

/employees/{emp_id}

  name: "Alice Hamilton",
  department: 461,
  start_date: <timestamp>

/employees/{emp_id}/private/finances

    salary: 80000,
    bonus_mult: 1.25,
    perf_review: 4.2

然后,您可以为这两个不同集合添加不同访问权限级别的安全规则。在本示例中,我们将使用自定义身份验证声明,以指明只有自定义身份验证声明 role 等于 Finance 的用户才能查看员工的财务信息。

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow any logged in user to view the public employee data
    match /employees/{emp_id} {
      allow read: if request.resource.auth != null
      // Allow only users with the custom auth claim of "Finance" to view
      // the employee's financial data
      match /private/finances {
        allow read: if request.resource.auth &&
          request.resource.auth.token.role == 'Finance'
      }
    }
  }
}

限制文档创建时包含的字段

Cloud Firestore 是无架构的,也就是说,数据库级别对文档包含什么字段没有任何限制。虽然这种灵活性使开发更简单,但有时候,您需要确保客户端只能创建包含特定字段或不包含其他字段的文档。

如需创建这类规则,您可以通过检查 request.resource.data 对象的 keys 方法。这是客户端尝试在此新文档中写入的所有字段的列表。通过将这组字段与 hasOnly()hasAny() 等函数结合使用,您可以添加逻辑来限制用户可以添加到 Cloud Firestore 的文档类型。

要求新文档中包含特定字段

假设您希望确保 restaurant 集合中创建的所有文档至少包含 namelocationcity 字段。为此,您可以对新文档中的键列表调用 hasAll()

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document contains a name
    // location, and city field
    match /restaurant/{restId} {
      allow create: if request.resource.data.keys().hasAll(['name', 'location', 'city']);
    }
  }
}

此方法还允许在创建 restaurant 集合时包含其他字段,但会确保客户端创建的所有文档至少包含这三个字段。

禁止在新文档中包含特定字段

同样,您也可以通过对禁止包含的字段列表使用 hasAny(),来阻止客户端创建包含特定字段的文档。如果文档中包含其中任一字段,则此方法的求值结果为 true,因此您可能需要对此结果取反,以禁止包含某些字段。

在以下示例中,客户端不可以创建包含 average_scorerating_count 字段的文档,因为这些字段稍后由服务器调用添加。

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document does *not*
    // contain an average_score or rating_count field.
    match /restaurant/{restId} {
      allow create: if (!request.resource.data.keys().hasAny(
        ['average_score', 'rating_count']));
    }
  }
}

为新文档创建允许字段的列表

有时,您可能想要创建仅包含新文档中明确允许包含字段的列表,而不是禁止新文档中包含某些字段。您可以使用 hasOnly() 函数确保创建的任何新文档仅包含这些字段(或这些字段的子集)而不包含任何其他字段。

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document doesn't contain
    // any fields besides the ones listed below.
    match /restaurant/{restId} {
      allow create: if (request.resource.data.keys().hasOnly(
        ['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

合并必填字段和可选字段

您可以在安全规则中合并使用 hasAllhasOnly 操作,以要求包含某些字段并允许包含其他字段。例如,此示例要求所有新文档都包含 namelocationcity 字段,并允许选择性地包含 addresshourscuisine 字段。

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document has a name,
    // location, and city field, and optionally address, hours, or cuisine field
    match /restaurant/{restId} {
      allow create: if (request.resource.data.keys().hasAll(['name', 'location', 'city'])) &&
       (request.resource.data.keys().hasOnly(
           ['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

在实际场景中,您可能希望将此逻辑移至辅助函数中,避免出现重复代码,并可更轻松地将可选字段和必需字段合并到一个列表中,如下所示:

service cloud.firestore {
  match /databases/{database}/documents {
    function verifyFields(required, optional) {
      let allAllowedFields = required.concat(optional);
      return request.resource.data.keys().hasAll(required) &&
        request.resource.data.keys().hasOnly(allAllowedFields);
    }
    match /restaurant/{restId} {
      allow create: if verifyFields(['name', 'location', 'city'],
        ['address', 'hours', 'cuisine']);
    }
  }
}

限制更新时包含的字段

常见的安全做法是只允许客户端修改某些字段而不得修改其他字段。仅使用上一部分中介绍的 request.resource.data.keys() 列表无法实现此目的,因为此列表代表的是更新后的完整文档的内容,因而包括客户端并未更改的字段。

但如果使用 diff() 函数,您可以将 request.resource.dataresource.data 对象进行比较,后者表示数据库中更新前的文档。这将创建一个 mapDiff 对象,其中包含这两个不同映射之间的所有更改。

通过对此 mapDiff 调用 affectedKeys() 方法,您可以得到一组在修改操作中发生了更改的字段。然后,您可以使用 hasOnly()hasAny() 等函数来确保此集合包含(或不包含)某些内容。

禁止更改某些字段

通过在 affectedKeys() 生成的集合上使用 hasAny() 方法然后将结果取反,您可以拒绝任何尝试更改您不想更改的字段的客户端请求。

例如,您可能想允许客户端更新某家餐厅的信息,但不允许更改其平均分数或评价。

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
      // Allow the client to update a document only if that document doesn't
      // change the average_score or rating_count fields
      allow update: if (!request.resource.data.diff(resource.data).affectedKeys()
        .hasAny(['average_score', 'rating_count']));
    }
  }
}

仅允许更改某些字段

您还可以使用 hasOnly() 函数来指定您要更改的字段的列表,而不是指定不想更改的字段。除非在安全规则中明确允许,否则这种做法默认不允许对任何新文档字段进行写入,因此通常更为安全。

例如,您可以创建安全规则,仅允许客户端更改 namelocationcityaddresshourscuisine 字段,而不是禁止更改 average_scorerating_count 字段。

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
    // Allow a client to update only these 6 fields in a document
      allow update: if (request.resource.data.diff(resource.data).affectedKeys()
        .hasOnly(['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

这样,如果未来您的应用进行了迭代,餐厅文档包括了 telephone 字段,那么除非您返回并将该字段添加至安全规则中的 hasOnly() 列表,否则对该字段的修改都会失败。

强制设置字段类型

Cloud Firestore 无架构的另一个影响是,对于在特定字段中可以存储哪些类型的数据,数据库级别并没有强制要求。但是,您可以通过 is 运算符在安全规则中强制执行此限制。

例如,以下安全规则强制要求评价的 score 字段必须为整数,headlinecontentauthor_name 字段都是字符串,review_date 是时间戳。

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
      // Restaurant rules go here...
      match /review/{reviewId} {
        allow create: if (request.resource.data.score is int &&
          request.resource.data.headline is string &&
          request.resource.data.content is string &&
          request.resource.data.author_name is string &&
          request.resource.data.review_date is timestamp
        );
      }
    }
  }
}

is 运算符的有效数据类型有 boolbytesfloatintlistlatlngnumberpathmapstringtimestampis 运算符还支持 constraintdurationsetmap_diff 数据类型,但由于这些类型是由安全规则语言本身生成的,而非由客户端生成,因此在大多数实际应用中极少用到它们。

listmap 数据类型不支持常规或类型参数。也就是说,您可以使用安全规则强制要求特定字段包含列表或映射,但不能强制该字段包含一个由所有整数或所有字符串组成的列表。

同样,您可以使用安全规则强制设置列表或映射中的特定条目的类型值(分别使用方括号表示法或键名),但没有一种快捷方式可以一次性强制设置映射或列表中所有成员的数据类型。

例如,以下规则可确保文档中的 tags 字段包含列表,且该列表的第一个条目是字符串;还会确保 product 字段包含一个映射,该映射又包含一个产品名称(字符串)和一个数量(整数)。

service cloud.firestore {
  match /databases/{database}/documents {
  match /orders/{orderId} {
    allow create: if request.resource.data.tags is list &&
      request.resource.data.tags[0] is string &&
      request.resource.data.product is map &&
      request.resource.data.product.name is string &&
      request.resource.data.product.quantity is int
      }
    }
  }
}

应在创建和更新文档时强制设置字段类型。因此,您可以考虑创建一个辅助函数,以便在安全规则的创建部分和更新部分进行调用。

service cloud.firestore {
  match /databases/{database}/documents {

  function reviewFieldsAreValidTypes(docData) {
     return docData.score is int &&
          docData.headline is string &&
          docData.content is string &&
          docData.author_name is string &&
          docData.review_date is timestamp;
  }

   match /restaurant/{restId} {
      // Restaurant rules go here...
      match /review/{reviewId} {
        allow create: if reviewFieldsAreValidTypes(request.resource.data) &&
          // Other rules may go here
        allow update: if reviewFieldsAreValidTypes(request.resource.data) &&
          // Other rules may go here
      }
    }
  }
}

强制设置可选字段的类型

请务必注意,如果文档不存在 foo 字段,对其调用 request.resource.data.foo 会导致错误,因此任何进行该调用的安全规则都会拒绝请求。您可以通过对 request.resource.data 使用 get 方法来处理这种情况。利用 get 方法,您可为从映射中检索的不存在的字段提供默认参数。

例如,如果评价文档中还包含一个可选 photo_url 字段和一个可选 tags 字段,并且您要验证这两个字段是否分别是字符串和列表,则可以通过将 reviewFieldsAreValidTypes 函数重写为以下内容来实现这一目的:

  function reviewFieldsAreValidTypes(docData) {
     return docData.score is int &&
          docData.headline is string &&
          docData.content is string &&
          docData.author_name is string &&
          docData.review_date is timestamp &&
          docData.get('photo_url', '') is string &&
          docData.get('tags', []) is list;
  }

这样会拒绝存在 tags 字段但类型并非列表的文档,同时仍然允许不包含 tags(或 photo_url)字段的文档。

绝不允许部分写入

关于 Cloud Firestore Security Rules 的最后一条注意事项是:它们要么允许客户端更改文档,要么拒绝整个修改操作。您无法创建在同一操作中接受对文档中某些字段的写入而拒绝针对其他字段的写入的安全规则。