安全规则的工作原理

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

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

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

Cloud Firestore

基本结构

Cloud Firestore 和 Cloud 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,则允许请求。

安全规则版本 2

Firebase 安全规则的版本 2 已于 2019 年 5 月起开始生效。版本 2 的递归通配符 {name=**} 行为有变化。如果计划使用集合组查询,必须使用版本 2。您必须将 rules_version = '2'; 置于安全规则的第一行来选择启用版本 2:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

匹配路径

所有 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

但请注意,递归通配符的行为取决于规则版本。

版本 1

安全规则默认使用版本 1。在版本 1 中,递归通配符与一个或多个路径项匹配。它们不能与空路径匹配,因此 match /cities/{city}/{document=**} 将与子集合(而非 cities 集合)中的文档匹配,而 match /cities/{document=**} 将同时与 cities 集合和子集合中的文档匹配。

递归通配符必须位于匹配语句的末尾。

版本 2

在安全规则的版本 2 中,递归通配符与零个或更多路径项匹配。match/cities/{city}/{document=**} 将与任何子集合中的文档以及 cities 集合中的文档匹配。

您必须将 rules_version = '2'; 添加到安全规则的第一行来选择启用版本 2:

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

您最多只能在每个匹配语句中使用一个递归通配符,但在版本 2 中,可以将此通配符放在匹配语句的任何位置。例如:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Matches any document in the songs collection group
    match /{path=**}/songs/{song} {
      allow read, write: if <condition>;
    }
  }
}

如果您使用集合组查询,必须使用版本 2。请参阅安全地进行集合组查询

重叠的匹配语句

某个文档可以与多个 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 次调用。

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

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

嵌套 match 语句深度上限 10
在路径段中,可在一组嵌套 match 语句中使用的路径长度上限 100
可在一组嵌套 match 语句中使用的路径捕获变量数上限 20
函数调用深度上限 20
函数参数的数量上限 7
每个函数的 let 变量绑定数上限 10
递归或循环函数调用次数上限 0(不允许)
每个请求中评估的表达式数量上限 1000
规则集的大小上限 规则集必须符合以下两种大小限制:
  • 对于从 Firebase 控制台或使用 firebase deploy 从 CLI 发布的规则集文本源,其大小不得超过 256 KB。
  • 对于 Firebase 处理该源并在后端将其激活时生成的编译规则集,其大小不得超过 250 KB。

Cloud Storage

基本结构

Cloud Firestore 和 Cloud 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,则允许请求。

匹配路径

Cloud Storage 安全规则可匹配 (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 安全规则不采用级联形式,只有当请求的路径与已指定规则的路径匹配时才会对规则进行求值。

请求求值

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

request 对象还包含用户的唯一 ID 和 request.auth 对象中的 Firebase Authentication 载荷,我们将在文档的身份验证部分进行详细说明。

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

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

资源求值

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

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

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

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

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

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

安全规则限制

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

限制 详细信息
每个请求调用 firestore.exists()firestore.get() 的最大次数

对于单文档请求和查询请求,该次数限制为 2 次。

超过此限制会导致权限遭拒的错误。

对同一文档的访问调用可能会被缓存,且缓存的调用不会计入限额。

完整示例

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

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

Realtime Database

基本结构

在 Realtime Database 中,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 时才允许请求的条件。

规则应用于路径的方式

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

请参考以下规则:

{
  "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 规则支持使用 $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 }
      }
    }
  }