Firestore安全规则实现列表仅返回当前用户参与的会话
问题根因
- Firestore安全规则本身不具备自动过滤数据的能力,不会在查询时遍历集合剔除无权限文档,只会在查询发起前做静态校验:如果查询存在返回越权数据的可能,就会直接拒绝,不会执行查询。
- 你写的测试代码2无任何过滤条件直接拉取全量conversations集合,必然会返回当前用户不在participants列表里的房间,被规则拦截是预期行为,正好符合你禁止全量拉取的需求。
- 测试代码1被拦截的核心原因:
- 现有规则没有校验查询中
whereArrayContains传入的参数是否为当前登录用户的邮箱,规则无法确认你不会传入其他用户的邮箱越权拉取房间列表,因此判定查询不安全。 - 你在查询中使用了
type字段过滤、timestamp字段排序,但现有规则没有对这些查询条件做合法性校验,规则无法确认这些条件不会导致越权。 - 原规则中
authed()函数的判断顺序有问题:未先判断request.auth是否为空就直接访问request.auth.token属性,未登录场景下会直接抛出规则执行错误,也会导致权限校验失败。
- 现有规则没有校验查询中
修正方案
不需要在list规则中逐文档判断用户是否在房间参与人列表里,只要通过request.query对象强制校验所有list查询的过滤条件符合权限约束即可,这种方式性能更好,也不会误拦截合法查询:
- 强制所有conversations集合的list查询必须携带
whereArrayContains("participants", 当前登录用户邮箱)过滤条件,从规则层面禁止用户传入其他用户的邮箱发起查询。 - 强制查询必须携带
whereEqualTo("visible", true)过滤条件,匹配房间可见性要求。 - 单文档get操作保留逐文档权限校验,防止用户通过已知房间ID越权拉取单个房间信息。
- 修复authed函数的判空顺序问题,避免未登录场景下规则执行报错。
修正后的安全规则代码如下:
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { function authed() { return request.auth != null && request.auth.token != null && request.auth.token.email != null && request.auth.token.firebase.sign_in_provider == 'password'; } function dataExists() { return resource != null && resource.data != null; } // 校验查询是否包含指定的where过滤条件 function hasWhere(filterField, filterOp, filterValue) { return request.query.where[filterField][filterOp] == filterValue; } // 校验list查询的所有强制过滤条件 function isValidConversationListQuery() { let userEmail = request.auth.token.email; return authed() // 必须查询可见房间 && hasWhere('visible', '==', true) // 必须查询当前用户参与的房间,禁止传入其他邮箱 && hasWhere('participants', 'array-contains', userEmail) // 限制合法排序字段,避免通过异常排序绕过权限 && (request.query.orderBy.size() == 0 || request.query.orderBy[0].field == 'timestamp' || request.query.orderBy[0].field == 'type'); } function isConversationVisible() { return dataExists() && resource.data.visible == true; } function userInConversation() { let email = request.auth.token.email; return dataExists() && email in resource.data.participants; } function userInConversationUsingGet(convId) { let email = request.auth.token.email; let conv = get(/databases/$(database)/documents/conversations/$(convId)); return conv.data != null && email in conv.data.participants && conv.data.visible == true; } match /conversations/{convId} { allow create, delete, update: if false; // list操作走查询条件校验,get操作走单文档权限校验 allow list: if isValidConversationListQuery(); allow get: if authed() && isConversationVisible() && userInConversation(); match /messages/{messageId} { allow create, read, list: if userInConversationUsingGet(convId); allow delete, update: if false; } } } }
客户端适配说明
- 你的测试代码1中
whereArrayContains传入的邮箱必须是当前登录用户的真实邮箱,即从FirebaseAuth.getInstance().getCurrentUser().getEmail()获取,不能硬编码字符串,否则会因为和规则中request.auth.token.email不匹配被拦截。 - 带
type过滤、timestamp排序的组合查询需要在Firebase控制台创建对应的复合索引,首次执行查询时报错信息里会带索引创建入口,创建完成即可正常查询,不属于权限问题。 - 规则生效后,所有无过滤的全量查询、传入非本人邮箱的查询都会被直接拦截,完全满足防止越权拉取全平台房间数据的需求。
内容的提问来源于stack exchange,提问作者CeccoCQ
相关产品推荐
相关产品推荐

