查询含数组过滤条件时如何校验Firestore LIST请求安全规则
我已搭建配置了标准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个匹配值),可以给文档增加反向映射冗余字段,改用等值查询实现:
- 文档结构调整,在原有acl字段外新增角色映射字段:
doc = { acl: ["role2", "role3"], acl_roleMap: { "role2": true, "role3": true } // 其余业务字段 }
- 前端查询调整,根据用户角色拼接查询条件:
// 单角色场景直接用等值查询,多角色场景可合并多个查询结果 db.collection('docs').where(`acl_roleMap.${userRole}`, '==', true)
- 规则写法调整,校验查询必须携带用户所属角色的等值条件:
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

