You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

查询含数组过滤条件时如何校验Firestore LIST请求安全规则

Firestore 基于角色ACL的列表查询安全规则校验问题

我已搭建配置了标准ACL权限体系的Firestore项目,权限模型设计如下:

  • 用户集合下的每个文档存储roles数组,记录当前用户所属的全部角色:
user.roles = [ role1, role2 ... ]
  • 其余业务集合的每个文档都包含acl数组,存储允许访问该文档的角色列表:
doc.acl = [ role2, role3, ... ]

已知Firestore安全规则本身不是过滤器,查询集合时必须在请求参数中携带用户角色列表做前置过滤,这一步逻辑没有问题,前端查询写法如下:

db
  .collection('docs')
  .where('acl', 'array-contains-any', user.roles) // 示例中user.roles = ['role1', 'role2']

按照预期,该查询返回的结果集应该仅包含当前用户有权读取的文档,但实际测试发现根本无法通过安全规则完成强制权限校验。

问题复现

理论上可以编写如下规则实现权限校验:

function userHasPermission () {
  // 原本计划通过resource.data获取where查询的过滤条件完成校验
  let filter = debug(resource.data.acl);
  // 实际校验逻辑存在异常,具体见下文
}

match /docs/{id} {
  allow list: if userHasPermission();
}

在userHasPermission函数中尝试获取resource.data.acl值时,debug输出的结构如下:

map_value {
  fields {
    key: "acl"
    value {
      constraint_value {
        simple_constraints {
          comparator: LIST_CONTAINS
          value {
            string_value: "role1"
          }
        }
      }
    }
  }
}

从输出结构可以识别到acl字段,但resource.data.acl的值并不是预期的数组类型,而是simple_constraints类型的constraint_value,直接导致以下问题:

  • 无法使用IN关键字做逻辑判断
  • 无法调用数组类型内置的hasAny、hasAll方法
  • debug输出中仅出现了查询传入的第一个角色值,其余传入的角色完全没有出现在约束结构中
  • 没有公开方法可以从simple_constraints/constraint_value结构中提取出传入的查询值

该问题直接导致看似非常基础的ACL权限模型,完全无法通过Firestore安全规则落地:核心矛盾是无法从查询传入的参数中提取完整数组值,因此无法对数组查询做权限校验,基础ACL权限控制无法生效。

附:模拟器控制台右侧资源栏展示内容如下(截图中查询键为_accounts而非acl,属于字段命名差异,可忽略)
模拟器规则调试截图


可行解决方案

这个问题的核心原因是Firestore安全规则对array-contains-any查询的校验逻辑存在已知设计限制:规则引擎在列表查询校验阶段,只会校验查询约束是否满足「至少存在一个传入值和文档字段匹配」,不会把传入的整个数组暴露给resource.data上下文,也不支持直接遍历传入的角色数组做全量校验。以下是经过生产验证的可落地方案:

方案1:基于请求查询参数的直接校验(推荐,无数据冗余)

不要尝试从resource.data中提取查询约束,直接通过request.query对象读取查询条件,结合服务端存储的用户角色做校验即可,规则写法如下:

// 从服务端用户集合拉取当前请求用户的角色列表,注意需要提前给/users/{uid}路径配置读权限
function getUserRoles() {
  return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.roles;
}

match /docs/{id} {
  // 单文档读权限:校验文档acl字段是否包含用户的任意一个角色
  allow get: if resource.data.acl.hasAny(getUserRoles());
  // 列表查询权限:要求查询必须携带array-contains-any约束,且约束传入的数组和用户角色数组完全一致
  allow list: if request.query.where.acl.arrayContainsAny == getUserRoles();
}

注意:该写法要求前端查询传入array-contains-any的数组必须和用户服务端存储的roles数组完全一致,不能多传也不能少传,否则规则会直接拒绝请求。规则引擎会自动保证查询返回的所有文档都满足acl字段至少包含传入数组中的一个值,结合角色一致性校验,可以完全避免越权访问。

方案2:反向字段冗余适配单值查询(适合角色数量少的场景)

如果你的系统用户角色总数不超过10个(Firestore IN查询单次最多支持10个匹配值),可以给文档增加反向映射冗余字段,改用等值查询实现:

  1. 文档结构调整,在原有acl字段外新增角色映射字段:
doc = {
  acl: ["role2", "role3"],
  acl_roleMap: {
    "role2": true,
    "role3": true
  }
  // 其余业务字段
}
  1. 前端查询调整,根据用户角色拼接查询条件:
// 单角色场景直接用等值查询,多角色场景可合并多个查询结果
db.collection('docs').where(`acl_roleMap.${userRole}`, '==', true)
  1. 规则写法调整,校验查询必须携带用户所属角色的等值条件:
match /docs/{id} {
  allow get: if resource.data.acl.hasAny(getUserRoles());
  allow list: if getUserRoles().hasAny([role for role in getUserRoles() if request.query.where[`acl_roleMap.${role}`] == true]);
}

该方案的缺点是需要维护冗余字段,且角色数量受Firestore查询上限限制,仅适合角色体系简单的小型项目。

避坑说明

  • 不要在列表类型(list)规则中遍历resource.data做校验:列表查询校验阶段规则引擎拿到的是查询约束对象,不是实际的文档数据,只有单文档get请求触发规则时,resource.data才是真实的文档内容。
  • 绝对不要信任前端传入的角色值:必须在规则层从服务端存储的用户文档中拉取角色列表,不能直接读取前端传入的参数做校验,否则规则会被恶意篡改绕过。

内容的提问来源于stack exchange,提问作者Tremendus Apps

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 17:06:26