在 Realtime Database 安全规则中使用条件

本指南以学习核心 Firebase 安全规则语言指南为基础,介绍如何向 Firebase Realtime Database 安全规则添加条件。

Realtime Database 安全规则的主要构成元素是条件。条件是一个布尔表达式,用于确定应该允许还是拒绝执行特定操作。对于基本规则,最好使用 truefalse 字面量作为条件。但是,“Realtime Database 安全规则”语言为您提供了编写更复杂条件的方法,这些条件可以:

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

使用 $ 变量采集路径段

您可以通过使用 $ 前缀声明采集变量来采集供读取或写入的路径部分。此变量充当通配符,用于存储可以在规则条件内部使用的键的值:

{
  "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 规则,该规则确保除了 titlecolor 之外,widget 没有其他子节点。任何会导致创建其他子节点的写入均将失败。

{
  "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 Authentication,则 request.auth 变量包含供客户端请求数据的身份验证信息。如需详细了解 request.auth,请参阅参考文档

Firebase Authentication 与 Firebase Realtime Database 集成,可让您使用条件按用户控制数据访问权限。用户通过身份验证后,系统即会使用用户的信息填充您的 Realtime Database 安全规则中的 auth 变量。这些信息包括其唯一标识符 (uid) 以及关联的帐号数据,例如 Facebook ID 或电子邮件地址以及其他信息。如果您实现了自定义身份验证提供方,则可以将自己的字段添加到用户的身份验证载荷中。

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

auth 变量

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

用户通过 Firebase Authentication 进行身份验证之后,它将包含以下特性:

provider 所使用的身份验证方法(“password”、“anonymous”、“facebook”、“github”、“google”或“twitter”)。
uid 唯一用户 ID,在所有提供方之间保证其唯一性。
token Firebase Auth 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"
      }
    }
  }
}

设计数据库的结构以支持身份验证条件

设计数据库结构可以让您更轻松地编写安全规则。在 Realtime Database 中存储用户数据的一种常见模式:将所有用户存储在单个 users 节点中,此节点的子节点是各个用户的 uid 值。如果您想限制对这些数据的访问,以便仅允许已登录用户查看自己的数据,您的规则将类似于以下示例。

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

使用身份验证自定义声明

对于需要针对不同用户进行自定义访问权限控制的应用,Firebase Authentication 允许开发者对 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 表示正在写入的新数据与现有数据的合并结果。

为便于说明,该规则允许我们创建新记录或删除现有记录,但不允许对现有的非 null 数据做出更改:

// 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 Realtime Database 安全规则而言十分重要。请参考以下规则:

{
  "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];
Swift
注意:此 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);
Java
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);
REST
# 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];
Swift
注意:此 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)
Java
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);
REST
# 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)

Realtime Database 安全规则中提供以下 query. 表达式。

基于查询的规则表达式
表达式 类型 说明
query.orderByKey
query.orderByPriority
query.orderByValue
布尔值 对于按键、优先级或值排序的查询,为 true。否则为 false。
query.orderByChild 字符串
null
使用字符串来表示到一个子节点的相对路径。例如,query.orderByChild === "address/zip"。如果查询并非按子节点排序,则此值为 null。
query.startAt
query.endAt
query.equalTo
字符串
数字
布尔值
null
检索正在执行的查询的范围,如果没有设置范围,则返回 null。
query.limitToFirst
query.limitToLast
数字
null
检索对正在执行的查询的限制,如果没有设置限制,则返回 null。

后续步骤

在讨论条件之后,您将对规则有更深入的了解,而且可以:

了解如何处理核心使用场景以及了解开发、测试和部署规则的工作流程

了解 Realtime Database 特有的规则功能