安全规则的工作原理

安全性可能是应用开发难题中最复杂的部分之一。在大多数应用中,开发者所构建和运行的服务器必须能够处理身份验证(用户是谁)和授权(用户可以做什么)这两大问题。

Firebase 安全规则移除中间(服务器)层,并允许您为直接连接到您的数据的客户端指定基于路径的权限。使用本指南可详细了解规则应用于传入请求的方式。

选择一种产品以详细了解其规则。

Cloud Firestore

基本结构

Cloud Firestore 和 Storage 中的 Firebase 安全规则使用以下结构和语法:

service <<name>> {
  // Match the resource path.
  match <<path>> {
    // Allow the request if the following conditions are true.
    allow <<methods>> : if <<condition>>
  }
}

在您构建规则时,了解以下关键概念非常重要:

  • 请求allow 语句中调用的一种或多种方法。这些是您获准运行的方法。标准方法包括 getlistcreateupdatedeletereadwrite 简便方法启用对指定的数据库或存储路径的广泛读写权限。
  • 路径:数据库或存储位置,表示为 URI 路径。
  • 规则allow 语句,包括在计算结果为 true 时才允许请求的条件。

匹配路径

所有 match 语句都应指向文档,而不是集合。Match 语句可以指向特定的文档(如 match /cities/SF),也可以使用通配符指向指定路径下的任意文档(如 match /cities/{city})。

在上面的示例中,match 语句使用了 {city} 通配符语法。这意味着相应规则适用于 cities 集合中的任意文档(例如 /cities/SF/cities/NYC)。在对 match 语句中的 allow 表达式求值时,city 变量将解析为城市文档名称(例如 SFNYC)。

匹配子集

Cloud Firestore 中的数据以文档集合的形式整理,每个文档可以通过子集合来扩展这种层次结构。了解安全规则如何与分层数据进行交互是非常重要的。

假设 cities 集合中的每个文档都包含一个 landmarks 子集合。由于安全规则仅适用于匹配的路径,因此在 cities 集合上定义的访问权限控制不适用于 landmarks 子集合。不过,您可以编写显式规则来控制对子集合的访问:

service cloud.firestore {
  match /databases/{database}/documents {
    match /cities/{city} {
      allow read, write: if <condition>;

        // Explicitly define rules for the 'landmarks' subcollection
        match /landmarks/{landmark} {
          allow read, write: if <condition>;
        }
    }
  }
}

嵌套 match 语句时,内层 match 语句的路径始终是相对于外层 match 语句而言的。因此,以下规则集是等效的:

service cloud.firestore {
  match /databases/{database}/documents {
    match /cities/{city} {
      match /landmarks/{landmark} {
        allow read, write: if <condition>;
      }
    }
  }
}
service cloud.firestore {
  match /databases/{database}/documents {
    match /cities/{city}/landmarks/{landmark} {
      allow read, write: if <condition>;
    }
  }
}

如果您要将规则应用于任意深度的层次结构,请使用递归通配符语法 {name=**}

service cloud.firestore {
  match /databases/{database}/documents {
    // Matches any document in the cities collection as well as any document
    // in a subcollection.
    match /cities/{document=**} {
      allow read, write: if <condition>;
    }
  }
}

使用递归通配符语法时,通配符变量将包含整个匹配的路径部分,即使相应文档位于多层嵌套的子集合中也是如此。例如,上面列出的规则将与位于 /cities/SF/landmarks/coit_tower 中的文档匹配,document 变量的值将是 SF/landmarks/coit_tower

递归通配符不能与空路径匹配,因此 match /cities/{city}/{document=**} 将与子集合(而非 cities 集合)中的文档匹配,而 match /cities/{document=**} 将同时与 cities 集合和子集合中的文档匹配。

某个文档可以与多个 match 语句匹配。如果有多个 allow 表达式匹配某个请求,则只要任何一个条件为 true,就允许访问:

service cloud.firestore {
  match /databases/{database}/documents {
    // Matches any document in the 'cities' collection.
    match /cities/{city} {
      allow read, write: if false;
    }

    // Matches any document in the 'cities' collection or subcollections.
    match /cities/{document=**} {
      allow read, write: if true;
    }
  }
}

在上面的示例中,因为第二个规则始终为 true,所以系统允许对 cities 集合进行所有读写操作,即使第一个规则始终为 false 也是如此。

安全规则限制

在处理安全规则时,请注意以下限制:

限制 详细信息
每个请求调用 exists()get()getAfter() 的最大次数
  • 10 表示单文档请求和查询请求。
  • 20 表示多文档读取、处理和批量写入。之前 10 的限制也适用于每个操作。

    例如,假设您创建了一个包含 3 个写入操作的批量写入请求,并且您的安全规则使用 2 个文档访问调用来验证每个写入操作。在这种情况下,每次写入操作使用 10 个访问调用中的 2 个,并且批量写入请求使用 20 个访问调用中的 6 个。

超过任一限制都会导致权限被拒绝的错误。

某些文档访问调用可能会被缓存,并且缓存的调用不会计入限制。

函数调用深度上限 20
递归或循环函数调用次数上限 0(不允许)
每个请求计算出的最大表达式数 1,000
规则集的大小上限 64 KB

Cloud Storage

基本结构

Cloud Firestore 和 Storage 中的 Firebase 安全规则使用以下结构和语法:

service <<name>> {
  // Match the resource path.
  match <<path>> {
    // Allow the request if the following conditions are true.
    allow <<methods>> : if <<condition>>
  }
}

在您构建规则时,了解以下关键概念非常重要:

  • 请求allow 语句中调用的一种或多种方法。这些是您获准运行的方法。标准方法包括 getlistcreateupdatedeletereadwrite 简便方法启用对指定的数据库或存储路径的广泛读写权限。
  • 路径:数据库或存储位置,表示为 URI 路径。
  • 规则allow 语句,包括在计算结果为 true 时才允许请求的条件。

匹配路径

存储安全规则可匹配 (match) 用于访问 Cloud Storage 中文件的文件路径。规则可以匹配 (match) 确切路径或通配符路径。此外,还可以嵌套规则。如果匹配规则都不允许请求方法,或条件求得的值为 false,那么请求将遭拒。

完全匹配

// Exact match for "images/profilePhoto.png"
match /images/profilePhoto.png {
  allow write: if <condition>;
}

// Exact match for "images/croppedProfilePhoto.png"
match /images/croppedProfilePhoto.png {
  allow write: if <other_condition>;
}

嵌套匹配

// Partial match for files that start with "images"
match /images {
  // Exact match for "images/profilePhoto.png"
  match /profilePhoto.png {
    allow write: if <condition>;
  }

  // Exact match for "images/croppedProfilePhoto.png"
  match /croppedProfilePhoto.png {
    allow write: if <other_condition>;
  }
}

通配符匹配

规则还可用于使用通配符进行模式匹配 (match)。通配符是一个已命名变量,表示单字符串(如 profilePhoto.png)或多个路径段(如 images/profilePhoto.png)。

可以用大括号括住通配符名称(如 {string}),从而创建单字符串通配符。可以向通配符名称添加 =**(如 {path=**}),从而声明多段通配符。

// Partial match for files that start with "images"
match /images {
  // Exact match for "images/*"
  // e.g. images/profilePhoto.png is matched
  match /{imageId} {
    // This rule only matches a single path segment (*)
    // imageId is a string that contains the specific segment matched
    allow read: if <condition>;
  }

  // Exact match for "images/**"
  // e.g. images/users/user:12345/profilePhoto.png is matched
  // images/profilePhoto.png is also matched!
  match /{allImages=**} {
    // This rule matches one or more path segments (**)
    // allImages is a path that contains all segments matched
    allow read: if <other_condition>;
  }
}

如果一个文件有多个匹配规则,所有规则求得的值之间存在 OR 关系。也就是说,如果文件的任何一个匹配规则求得的值为 true,结果就为 true

在上述规则中,只要 conditionother_condition 中任何一个求得的值为 true,就可以读取文件“images/profilePhoto.png”;而只有当 other_condition 求得的值为 true 时,才能读取文件“images/users/user:12345/profilePhoto.png”。

可以从提供文件名或路径授权的 match 中引用通配符变量:

// Another way to restrict the name of a file
match /images/{imageId} {
  allow read: if imageId == "profilePhoto.png";
}

存储安全规则不采用级联形式,只有当请求的路径与已指定规则的路径匹配时才会对规则进行求值。

请求求值

上传、下载、元数据更改和删除操作是使用发送到 Cloud Storage 的 request 进行求值的。request 变量包含正在执行请求的文件路径、请求接收时间以及新的 resource 值(如果请求为写入操作)。此外,还包含 HTTP 标头和身份验证状态。

request 对象还包含用户的唯一 ID 和 request.auth 对象中的 Firebase 身份验证负载,我们将在相关文档的基于用户的安全性部分进一步阐述这一点。

request 对象中属性的完整列表如下:

属性 类型 说明
auth 映射<字符串, 字符串> 在用户登录后,提供 uid(用户的唯一 ID)和 token(Firebase 身份验证 JWT 声明的映射)。如果用户未登录,则为 null
params 映射<字符串, 字符串> 包含请求的查询参数的映射。
path 路径 表示正在执行请求的路径的 path
resource 映射<字符串, 字符串> 新资源值,仅当执行 write 请求时存在。
time 时间戳 时间戳,表示对请求进行求值时的服务器时间。

资源求值

对规则进行求值时,您可能还想要对正在上传、下载、修改或删除的文件的元数据进行求值。这样一来,您就可以创建功能强大的复杂规则来执行任务,例如仅允许上传包含特定内容类型的文件,或仅允许删除超过特定大小的文件。

面向 Cloud Storage 的 Firebase 安全规则在 resource 对象中提供文件元数据,其中包含 Cloud Storage 对象中提供的元数据键值对。可以在 readwrite 请求中检查这些属性,以确保数据完整性。

write 请求(如上传、元数据更新和删除请求)中,除了包含请求路径下现有文件的文件元数据的 resource 对象外,您还可以使用 request.resource 对象,其中包含在允许写入时要写入的一部分文件元数据。您可以使用这两个值来确保数据完整性,或强制实施应用约束(如文件类型或大小)。

resource 对象中属性的完整列表如下:

属性 类型 说明
name 字符串 此对象的完整名称
bucket 字符串 此对象所在存储分区的名称。
generation 整数 此对象的 GCS 对象生成
metageneration 整数 此对象的 GCS 对象元生成
size 整数 此对象的大小(以字节为单位)。
timeCreated 时间戳 表示此对象的创建时间的时间戳。
updated 时间戳 表示此对象的最后更新时间的时间戳。
md5Hash 字符串 此对象的 MD5 哈希。
crc32c 字符串 此对象的 crc32c 哈希。
etag 字符串 与此对象相关联的 etag。
contentDisposition 字符串 与此对象相关联的内容处置。
contentEncoding 字符串 与此对象相关联的内容编码。
contentLanguage 字符串 与此对象相关联的内容语言。
contentType 字符串 与此对象相关联的内容类型。
metadata 映射<字符串, 字符串> 开发者指定的其他自定义元数据的键值对。

request.resource 包含上述除 generationmetagenerationetagtimeCreatedupdated 外的所有属性。

完整示例

综上,您可以为图片存储解决方案创建完整的示例规则:

service firebase.storage {
 match /b/{bucket}/o {
   match /images {
     // Cascade read to any image type at any path
     match /{allImages=**} {
       allow read;
     }

     // Allow write files to the path "images/*", subject to the constraints:
     // 1) File is less than 5MB
     // 2) Content type is an image
     // 3) Uploaded content type matches existing content type
     // 4) File name (stored in imageId wildcard variable) is less than 32 characters
     match /{imageId} {
       allow write: if request.resource.size < 5 * 1024 * 1024
                    && request.resource.contentType.matches('image/.*')
                    && request.resource.contentType == resource.contentType
                    && imageId.size() < 32
     }
   }
 }
}

实时数据库

基本结构

在实时数据库中,Firebase 安全规则由 JSON 文档中包含的类似 JavaScript 的表达式组成。

它们使用以下语法:

{
  "rules": {
    "<<path>>": {
    // Allow the request if the condition for each method is true.
      ".read": <<condition>>,
      ".write": <<condition>>,
      ".validate": <<condition>>
    }
  }
}

规则中有三个基本元素:

  • 路径:数据库位置。这镜像了数据库的 JSON 结构。
  • 请求:这些是规则用于授予访问权限的方法。readwrite 规则授予广泛的读写权限,而 validate 规则充当辅助验证,以根据传入或现有数据授予访问权限。
  • 条件:在计算结果为 true 时才允许请求的条件。

规则应用于路径的方式

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

请参考以下规则:

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

位置变量

实时数据库规则支持使用 $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 }
      }
    }
  }