安全规则的工作原理(续11)

规则应用于路径的方式

在 Realtime Database 中,Security Rules 以原子方式应用,这意味着较高级别父节点上的规则会覆盖更细粒度的子节点上的规则,而较深层的节点上的规则无法授予对父路径的访问权限。如果已经为其中一个父路径授予了访问权限,则无法在数据库结构中的更深层路径上优化或撤消访问权限。

请参考以下规则:

{
  "rules": {
     "foo": {
        // allows read to /foo/*
        ".read": "data.child('baz').val() === true",
        "bar": {
          // ignored, since read was allowed already
          ".read": false
        }
     }
  }
}

只要 /foo/ 包含值为 true 的子节点 baz,此安全结构即允许读取 /bar/。/foo/bar/ 之下的 ".read": false 规则此时不起作用,因为子路径无法撤消访问权限。

虽然看起来不是很直观,但这是该规则语言十分强大的一个功能,可让您用最少的工作量实现非常复杂的访问特权。这对于基于用户的安全性尤其有用。

但是,.validate 规则不会级联。要执行写入,必须满足层次结构中各个层级的所有验证规则。

此外,由于规则不会应用于父路径,因此如果所请求的位置或授予访问权限的父位置没有规则,读取或写入操作将会失败。即使所有相关子路径均可访问,在父位置的读取操作也会完全失败。请参考以下结构:

{
  "rules": {
    "records": {
      "rec1": {
        ".read": true
      },
      "rec2": {
        ".read": false
      }
    }
  }
}

如果不理解规则是以原子方式进行求值,这看起来可能就像提取 /records/ 路径会返回 rec1,而不是 rec2。但实际的结果是一个错误:

JavaScript
var db = firebase.database();
db.ref("records").once("value", function(snap) {
  // success method is not called
}, function(err) {
  // error callback triggered with PERMISSION_DENIED
});
Objective-C

注意:此 Firebase 产品不适用于 App Clip 目标。

FIRDatabaseReference *ref = [[FIRDatabase database] reference];
[[_ref child:@"records"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
  // success block is not called
} withCancelBlock:^(NSError * _Nonnull error) {
  // cancel block triggered with PERMISSION_DENIED
}];
Swift

注意:此 Firebase 产品不适用于 App Clip 目标。

var ref = FIRDatabase.database().reference()
ref.child("records").observeSingleEventOfType(.Value, withBlock: { snapshot in
    // success block is not called
}, withCancelBlock: { error in
    // cancel block triggered with PERMISSION_DENIED
})
Java
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference("records");
ref.addListenerForSingleValueEvent(new ValueEventListener() {
  @Override
  public void onDataChange(DataSnapshot snapshot) {
    // success method is not called
  }

  @Override
  public void onCancelled(FirebaseError firebaseError) {
    // error callback triggered with PERMISSION_DENIED
  });
});
REST
curl https://docs-examples.firebaseio.com/rest/records/
# response returns a PERMISSION_DENIED error

由于在 /records/ 的读取操作以原子方式进行,且没有任何读取规则授予对 /records/ 下所有数据的访问权限,因此将产生 PERMISSION_DENIED 错误。如果在 Firebase 控制台的安全模拟器中对此规则求值,就可以看到该读取操作被拒绝:

Attempt to read /records with auth=Success(null)
    /
    /records

No .read rule allowed the operation.
Read was denied.

该操作被拒绝的原因是没有允许访问 /records/ 路径的读取规则。需要注意的是,对 rec1 的规则始终未被求值,因为它不在我们请求的路径中。要提取 rec1,我们需要直接对其进行访问:

JavaScript
var db = firebase.database();
db.ref("records/rec1").once("value", function(snap) {
  // SUCCESS!
}, function(err) {
  // error callback is not called
});
Objective-C

注意:此 Firebase 产品不适用于 App Clip 目标。

FIRDatabaseReference *ref = [[FIRDatabase database] reference];
[[ref child:@"records/rec1"] observeSingleEventOfType:FEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
    // SUCCESS!
}];
Swift

注意:此 Firebase 产品不适用于 App Clip 目标。

var ref = FIRDatabase.database().reference()
ref.child("records/rec1").observeSingleEventOfType(.Value, withBlock: { snapshot in
    // SUCCESS!
})
Java
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference("records/rec1");
ref.addListenerForSingleValueEvent(new ValueEventListener() {
  @Override
  public void onDataChange(DataSnapshot snapshot) {
    // SUCCESS!
  }

  @Override
  public void onCancelled(FirebaseError firebaseError) {
    // error callback is not called
  }
});
REST
curl https://docs-examples.firebaseio.com/rest/records/rec1
# SUCCESS!

位置变量

Realtime Database Security Rules 支持使用 $location 变量匹配路径段。使用路径段前面的 $ 前缀将您的规则与路径上的所有子节点相匹配。

  {
    "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')"
          }
        }
      }
    }
  }

您还可以将 $variable 与常量路径名并行使用。

  {
    "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 }
      }
    }
  }