实时数据库规则是针对数据库的声明性配置。这意味着这些规则的定义独立于产品逻辑之外。这种方法具有许多优点:客户端不负责强制执行安全策略,即使有缺陷的实现也不会损坏您的数据,而且或许最重要的一点是,无需中间受托对象(例如服务器)来保护数据。
组织您的规则
实时数据库规则由类似 Jvascript 的表达式(包含在 JSON 文件中)构成。规则的结构应符合您在数据库中存储的数据的结构。例如,假设您要跟踪消息列表,且您具有如下所示的数据:
{ "messages": { "message0": { "content": "Hello", "timestamp": 1405704370369 }, "message1": { "content": "Goodbye", "timestamp": 1405704395231 }, ... } }
您的规则应以类似的方式构建。下面是一个可能适用于此类数据结构的规则集示例。
{ "rules": { "messages": { "$message": { // only messages from the last ten minutes can be read ".read": "data.child('timestamp').val() > (now - 600000)", // new messages must have a string content and a number timestamp ".validate": "newData.hasChildren(['content', 'timestamp']) && newData.child('content').isString() && newData.child('timestamp').isNumber()" } } } }
安全规则类型
有三种规则可用于强制执行安全策略:.write
、.read
和 .validate
。下面简要介绍了这三种规则的用途:
规则类型 | |
---|---|
.read | 描述是否允许用户读取数据以及何时读取。 |
.write | 描述是否允许写入数据以及何时写入。 |
.validate | 定义值的正确格式、值是否包含子属性以及值的数据类型。 |
预定义变量
在安全规则定义中可访问很多有用的预定义变量。在下面的示例中,我们将使用其中大多数变量。下面简要总结了各个变量并提供了相应 API 参考的链接。
预定义变量 | |
---|---|
now | 当前时间,以从 Linux 计时原点开始计算的毫秒数表示。此变量非常适合验证使用 SDK 的 firebase.database.ServerValue.TIMESTAMP 创建的时间戳。 |
root | RuleDataSnapshot,表示在尝试操作之前存在于 Firebase 数据库中的根路径。 |
newData | RuleDataSnapshot,表示尝试操作后应存在的数据。该数据包含写入的新数据和现有数据。 |
data | RuleDataSnapshot,表示尝试操作之前存在的数据。 |
$ variables | 用于表示 ID 和动态子键的通配符路径。 |
auth | 表示通过身份验证的用户的令牌有效负载。 |
这些变量可用于您的规则的任何位置。例如,以下安全规则可确保写入 /foo/
节点的数据必须是少于 100 个字符的字符串:
{ "rules": { "foo": { // /foo is readable by the world ".read": true, // /foo is writable by the world ".write": true, // data written to /foo must be a string less than 100 characters ".validate": "newData.isString() && newData.val().length < 100" } } }
现有数据与新数据
预定义 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()"
引用其他路径的数据
任何数据都可用作规则标准。使用预定义变量 root
、data
和 newData
,我们可以访问在写入事件发生之前或之后存在的任何路径。
就此例而言,只要 /allow_writes/
节点的值为 true
,父节点没有 readOnly
标志设置,且新写入的数据中有一个名为 foo
的子节点,就允许执行写入操作:
".write": "root.child('allow_writes').val() === true && !data.parent().child('readOnly').exists() && newData.child('foo').exists()"
读取和写入规则级联
.read
和 .write
规则自上而下发挥作用,浅层规则将重写深层规则。如果某个规则授予对特定路径的读取或写入权限,则也会授予对此路径下所有子节点的访问权限。 请参考以下结构:
{ "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
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
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
FIRDatabaseReference *ref = [[FIRDatabase database] reference]; [[ref child:@"records/rec1"] observeSingleEventOfType:FEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { // SUCCESS! }];
Swift
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!
基于查询的规则
虽然不能将规则用作过滤条件,但您可以通过在规则中使用查询参数来限制对各个数据子集的访问权限。在您的规则中使用 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。否则为 false。 |
query.orderByChild | 字符串 null |
使用字符串来表示到一个子节点的相对路径。例如,query.orderByChild == "address/zip" 。如果查询并非按子节点排序,则此值为 null。
|
query.startAt query.endAt query.equalTo |
字符串 数字 布尔值 null |
检索正在执行的查询的范围,如果没有设置范围,则返回 null。 |
query.limitToFirst query.limitToLast |
数字 null |
检索对正在执行的查询的限制,如果没有设置限制,则返回 null。 |
验证数据
您应使用 .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
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
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 *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
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()
规则可能例外,该规则取决于是否允许删除。
使用 $ 变量采集路径段
您可以通过使用 $
前缀声明采集变量来采集供读取或写入的路径部分。此变量充当通配符,用于存储供在规则声明内部使用的键的值:
{ "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
规则,该规则确保除了 title
和 color
之外,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 } } } }
匿名聊天示例
让我们将规则汇总,创建一个安全的匿名聊天应用。我们将列出规则;包含聊天的功能性版本如下所示:
{ "rules": { // default rules are false if not specified // setting these to true would make ALL CHILD PATHS readable/writable // ".read": false, // ".write": false, "room_names": { // the room names can be enumerated and read // they cannot be modified since no write rule // explicitly allows this ".read": true, "$room_id": { // this is just for documenting the structure of rooms, since // they are read-only and no write rule allows this to be set ".validate": "newData.isString()" } }, "messages": { "$room_id": { // the list of messages in a room can be enumerated and each // message could also be read individually, the list of messages // for a room cannot be written to in bulk ".read": true, // room we want to write a message to must be valid ".validate": "root.child('room_names/'+$room_id).exists()", "$message_id": { // a new message can be created if it does not exist, but it // cannot be modified or deleted ".write": "!data.exists() && newData.exists()", // the room attribute must be a valid key in room_names/ (the room must exist) // the object to write must have a name, message, and timestamp ".validate": "newData.hasChildren(['name', 'message', 'timestamp'])", // the name must be a string, longer than 0 chars, and less than 20 and cannot contain "admin" "name": { ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length < 20 && !newData.val().contains('admin')" }, // the message must be longer than 0 chars and less than 50 "message": { ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length < 50" }, // messages cannot be added in the past or the future // clients should use firebase.database.ServerValue.TIMESTAMP // to ensure accurate timestamps "timestamp": { ".validate": "newData.val() <= now" }, // no other fields can be included in a message "$other": { ".validate": false } } } } } }