实时数据库规则中的使用条件

本指南以学习核心 Firebase 安全规则语言指南为基础,展示如何向 Firebase 实时数据库安全规则添加条件。

实时数据库安全规则的主要构建块是条件。条件是一个布尔表达式,用于确定是否应允许或拒绝特定操作。对于基本规则,使用true文字作为条件false有效。但是实时数据库安全规则语言为您提供了编写更复杂条件的方法,这些条件可以:

  • 检查用户身份验证
  • 根据新提交的数据评估现有数据
  • 访问和比较数据库的不同部分
  • 验证传入数据
  • 将传入查询的结构用于安全逻辑

使用 $ 变量捕获路径段

您可以通过使用$前缀声明捕获变量来捕获路径的一部分以进行读取或写入。这用作通配符,并存储该键的值以供在规则条件内使用:

{
  "rules": {
    "rooms": {
      // this rule applies to any child of /rooms/, the key for each room id
      // is stored inside $room_id variable for reference
      "$room_id": {
        "topic": {
          // the room's topic can be changed if the room id has "public" in it
          ".write": "$room_id.contains('public')"
        }
      }
    }
  }
}

动态$变量也可以与常量路径名并行使用。在这个例子中,我们使用$other变量来声明一个.validate规则,以确保widget除了titlecolor之外没有子级。任何会导致创建额外子项的写入都将失败。

{
  "rules": {
    "widget": {
      // a widget can have a title or color attribute
      "title": { ".validate": true },
      "color": { ".validate": true },

      // but no other child paths are allowed
      // in this case, $other means any key excluding "title" and "color"
      "$other": { ".validate": false }
    }
  }
}

验证

最常见的安全规则模式之一是根据用户的身份验证状态控制访问。例如,您的应用可能希望只允许登录用户写入数据。

如果您的应用使用 Firebase 身份验证,则request.auth变量包含客户端请求数据的身份验证信息。有关request.auth的更多信息,请参阅参考文档

Firebase 身份验证与 Firebase 实时数据库集成,允许您使用条件控制每个用户的数据访问。用户进行身份验证后,实时数据库安全规则规则中的auth变量将填充用户信息。此信息包括他们的唯一标识符 ( uid ) 以及关联的帐户数据,例如 Facebook id 或电子邮件地址,以及其他信息。如果您实现自定义身份验证提供程序,则可以将自己的字段添加到用户的身份验证负载中。

本部分介绍如何将 Firebase 实时数据库安全规则语言与有关您的用户的身份验证信息结合使用。通过结合这两个概念,您可以根据用户身份控制对数据的访问。

auth变量

在进行身份验证之前,规则中预定义的auth变量为 null。

一旦用户通过Firebase 身份验证,它将包含以下属性:

提供者使用的身份验证方法(“密码”、“匿名”、“facebook”、“github”、“google”或“twitter”)。
uid唯一的用户 ID,保证在所有提供商中都是唯一的。
令牌Firebase 身份验证 ID 令牌的内容。有关更多详细信息,请参阅auth.token的参考文档。

这是一个使用auth变量来确保每个用户只能写入用户特定路径的示例规则:

{
  "rules": {
    "users": {
      "$user_id": {
        // grants write access to the owner of this user account
        // whose uid must exactly match the key ($user_id)
        ".write": "$user_id === auth.uid"
      }
    }
  }
}

构建数据库以支持身份验证条件

以使编写规则更容易的方式来构建数据库通常很有帮助。在实时数据库中存储用户数据的一种常见模式是将所有用户存储在单个users节点中,其子节点是每个用户的uid值。如果您想限制对这些数据的访问,以便只有登录的用户可以看到他们自己的数据,您的规则看起来像这样。

{
  "rules": {
    "users": {
      "$uid": {
        ".read": "auth != null && auth.uid == $uid"
      }
    }
  }
}

使用身份验证自定义声明

对于需要针对不同用户进行自定义访问控制的应用,Firebase 身份验证允许开发人员对 Firebase 用户设置声明。这些声明可在您的规则中的auth.token变量中访问。以下是使用hasEmergencyTowel自定义声明的规则示例:

{
  "rules": {
    "frood": {
      // A towel is about the most massively useful thing an interstellar
      // hitchhiker can have
      ".read": "auth.token.hasEmergencyTowel === true"
    }
  }
}

创建自己的自定义身份验证令牌的开发人员可以选择向这些令牌添加声明。这些声明在您的规则中的auth.token变量上可用。

现有数据与新数据

预定义的data变量用于在写入操作发生之前引用数据。相反, newData变量包含写入操作成功时将存在的新数据。 newData表示正在写入的新数据和现有数据的合并结果。

举例来说,这条规则将允许我们创建新记录或删除现有记录,但不能更改现有的非空数据:

// we can write as long as old data or new data does not exist
// in other words, if this is a delete or a create, but not an update
".write": "!data.exists() || !newData.exists()"

引用其他路径中的数据

任何数据都可以用作规则的标准。使用预定义的变量rootdatanewData ,我们可以访问在写入事件之前或之后存在的任何路径。

考虑这个例子,它允许写入操作,只要/allow_writes/节点的值为true ,父节点没有设置readOnly标志,并且新写入的数据中有一个名为foo的子节点:

".write": "root.child('allow_writes').val() === true &&
          !data.parent().child('readOnly').exists() &&
          newData.child('foo').exists()"

验证数据

应使用.validate规则强制执行数据结构并验证数据的格式和内容,这些规则仅在.write规则成功授予访问权限后运行。下面是一个示例.validate规则定义,它只允许 1900-2099 年之间格式为 YYYY-MM-DD 的日期,使用正则表达式进行检查。

".validate": "newData.isString() &&
              newData.val().matches(/^(19|20)[0-9][0-9][-\\/. ](0[1-9]|1[012])[-\\/. ](0[1-9]|[12][0-9]|3[01])$/)"

.validate规则是唯一不级联的安全规则类型。如果任何子记录上的任何验证规则失败,则整个写入操作将被拒绝。此外,删除数据时(即,写入的新值为null时)忽略验证定义。

这些可能看起来微不足道,但实际上是编写强大的 Firebase 实时数据库安全规则的重要功能。考虑以下规则:

{
  "rules": {
    // write is allowed for all paths
    ".write": true,
    "widget": {
      // a valid widget must have attributes "color" and "size"
      // allows deleting widgets (since .validate is not applied to delete rules)
      ".validate": "newData.hasChildren(['color', 'size'])",
      "size": {
        // the value of "size" must be a number between 0 and 99
        ".validate": "newData.isNumber() &&
                      newData.val() >= 0 &&
                      newData.val() <= 99"
      },
      "color": {
        // the value of "color" must exist as a key in our mythical
        // /valid_colors/ index
        ".validate": "root.child('valid_colors/' + newData.val()).exists()"
      }
    }
  }
}

考虑到这种变体,请查看以下写入操作的结果:

JavaScript
var ref = db.ref("/widget");

// PERMISSION_DENIED: does not have children color and size
ref.set('foo');

// PERMISSION DENIED: does not have child color
ref.set({size: 22});

// PERMISSION_DENIED: size is not a number
ref.set({ size: 'foo', color: 'red' });

// SUCCESS (assuming 'blue' appears in our colors list)
ref.set({ size: 21, color: 'blue'});

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
ref.child('size').set(99);
Objective-C
注意:此 Firebase 产品不适用于 App Clip 目标。
FIRDatabaseReference *ref = [[[FIRDatabase database] reference] child: @"widget"];

// PERMISSION_DENIED: does not have children color and size
[ref setValue: @"foo"];

// PERMISSION DENIED: does not have child color
[ref setValue: @{ @"size": @"foo" }];

// PERMISSION_DENIED: size is not a number
[ref setValue: @{ @"size": @"foo", @"color": @"red" }];

// SUCCESS (assuming 'blue' appears in our colors list)
[ref setValue: @{ @"size": @21, @"color": @"blue" }];

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
[[ref child:@"size"] setValue: @99];
迅速
注意:此 Firebase 产品不适用于 App Clip 目标。
var ref = FIRDatabase.database().reference().child("widget")

// PERMISSION_DENIED: does not have children color and size
ref.setValue("foo")

// PERMISSION DENIED: does not have child color
ref.setValue(["size": "foo"])

// PERMISSION_DENIED: size is not a number
ref.setValue(["size": "foo", "color": "red"])

// SUCCESS (assuming 'blue' appears in our colors list)
ref.setValue(["size": 21, "color": "blue"])

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
ref.child("size").setValue(99);
爪哇
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference("widget");

// PERMISSION_DENIED: does not have children color and size
ref.setValue("foo");

// PERMISSION DENIED: does not have child color
ref.child("size").setValue(22);

// PERMISSION_DENIED: size is not a number
Map<String,Object> map = new HashMap<String, Object>();
map.put("size","foo");
map.put("color","red");
ref.setValue(map);

// SUCCESS (assuming 'blue' appears in our colors list)
map = new HashMap<String, Object>();
map.put("size", 21);
map.put("color","blue");
ref.setValue(map);

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
ref.child("size").setValue(99);
休息
# PERMISSION_DENIED: does not have children color and size
curl -X PUT -d 'foo' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# PERMISSION DENIED: does not have child color
curl -X PUT -d '{"size": 22}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# PERMISSION_DENIED: size is not a number
curl -X PUT -d '{"size": "foo", "color": "red"}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# SUCCESS (assuming 'blue' appears in our colors list)
curl -X PUT -d '{"size": 21, "color": "blue"}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# If the record already exists and has a color, this will
# succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
# will fail to validate
curl -X PUT -d '99' \
https://docs-examples.firebaseio.com/rest/securing-data/example/size.json

现在让我们看一下相同的结构,但使用.write规则而不是.validate

{
  "rules": {
    // this variant will NOT allow deleting records (since .write would be disallowed)
    "widget": {
      // a widget must have 'color' and 'size' in order to be written to this path
      ".write": "newData.hasChildren(['color', 'size'])",
      "size": {
        // the value of "size" must be a number between 0 and 99, ONLY IF WE WRITE DIRECTLY TO SIZE
        ".write": "newData.isNumber() && newData.val() >= 0 && newData.val() <= 99"
      },
      "color": {
        // the value of "color" must exist as a key in our mythical valid_colors/ index
        // BUT ONLY IF WE WRITE DIRECTLY TO COLOR
        ".write": "root.child('valid_colors/'+newData.val()).exists()"
      }
    }
  }
}

在此变体中,以下任何操作都会成功:

JavaScript
var ref = new Firebase(URL + "/widget");

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
ref.set({size: 99999, color: 'red'});

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
ref.child('size').set(99);
Objective-C
注意:此 Firebase 产品不适用于 App Clip 目标。
Firebase *ref = [[Firebase alloc] initWithUrl:URL];

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
[ref setValue: @{ @"size": @9999, @"color": @"red" }];

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
[[ref childByAppendingPath:@"size"] setValue: @99];
迅速
注意:此 Firebase 产品不适用于 App Clip 目标。
var ref = Firebase(url:URL)

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
ref.setValue(["size": 9999, "color": "red"])

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
ref.childByAppendingPath("size").setValue(99)
爪哇
Firebase ref = new Firebase(URL + "/widget");

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
Map<String,Object> map = new HashMap<String, Object>();
map.put("size", 99999);
map.put("color", "red");
ref.setValue(map);

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
ref.child("size").setValue(99);
休息
# ALLOWED? Even though size is invalid, widget has children color and size,
# so write is allowed and the .write rule under color is ignored
curl -X PUT -d '{size: 99999, color: "red"}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# ALLOWED? Works even if widget does not exist, allowing us to create a widget
# which is invalid and does not have a valid color.
# (allowed by the write rule under "color")
curl -X PUT -d '99' \
https://docs-examples.firebaseio.com/rest/securing-data/example/size.json

这说明了.write.validate规则之间的区别。正如所展示的,所有这些规则都应该使用.validate编写,除了newData.hasChildren()规则可能例外,这取决于是否允许删除。

基于查询的规则

尽管您不能将规则用作过滤器,但您可以通过在规则中使用查询参数来限制对数据子集的访问。使用query.规则中的表达式以根据查询参数授予读取或写入访问权限。

例如,以下基于查询的规则使用基于用户的安全规则和基于查询的规则来限制对购物baskets集合中数据的访问,仅限活动用户拥有的购物篮:

"baskets": {
  ".read": "auth.uid != null &&
            query.orderByChild == 'owner' &&
            query.equalTo == auth.uid" // restrict basket access to owner of basket
}

以下查询(包括规则中的查询参数)将成功:

db.ref("baskets").orderByChild("owner")
                 .equalTo(auth.currentUser.uid)
                 .on("value", cb)                 // Would succeed

但是,不包含规则中参数的查询将失败并出现PermissionDenied错误:

db.ref("baskets").on("value", cb)                 // Would fail with PermissionDenied

您还可以使用基于查询的规则来限制客户端通过读取操作下载的数据量。

例如,以下规则将读取权限限制为仅查询的前 1000 个结果,按优先级排序:

messages: {
  ".read": "query.orderByKey &&
            query.limitToFirst <= 1000"
}

// Example queries:

db.ref("messages").on("value", cb)                // Would fail with PermissionDenied

db.ref("messages").limitToFirst(1000)
                  .on("value", cb)                // Would succeed (default order by key)

以下query.表达式在实时数据库安全规则中可用。

基于查询的规则表达式
表达类型描述
query.orderByKey
query.orderByPriority
query.orderByValue
布尔值True 对于按键、优先级或值排序的查询。否则为假。
query.orderByChild细绳
空值
使用字符串表示子节点的相对路径。例如, query.orderByChild == "address/zip" 。如果查询不是按子节点排序的,则此值为空。
查询.startAt
查询.endAt
查询.equalTo
细绳
数字
布尔值
空值
检索执行查询的边界,如果没有边界集,则返回 null。
query.limitToFirst
query.limitToLast
数字
空值
检索执行查询的限制,如果没有设置限制,则返回 null。

下一步

在对条件的讨论之后,您对规则有了更深入的了解,并准备好:

了解如何处理核心用例,并了解开发、测试和部署规则的工作流程:

了解特定于实时数据库的规则功能: