Flutter Firebase如何查询当前用户uid匹配的所有预约文档
核心问题原因
你遇到的权限拒绝、越权访问问题,本质是对Firestore安全规则的运行逻辑存在两个关键误解:
- Firestore安全规则不是数据过滤器:发起全量集合查询时,规则不会自动逐文档筛选你有权限访问的内容,只要查询结果可能包含任意一条无权限数据,整个查询会直接被拒绝。你之前不管用户角色直接拉取全量
bookings集合,触发权限报错就是这个原因。 - 你当前配置的规则仅校验了单文档的读权限,没有对集合查询的合法性做约束,同时
allow write: if true属于严重安全漏洞,会导致任意访问者篡改、删除全量预约数据,你之前碰到的越权问题基本都是这个配置导致的。
方案一:保留根级Bookings集合(推荐,适合需要跨用户统计、管理员全局查询的场景)
该方案不需要调整现有数据结构,仅需修改查询逻辑和安全规则即可,是多数预约类系统的通用选型。
1. 安全规则配置
规则会区分普通用户、管理员权限,且严格和查询条件做绑定校验,从底层拦截非法请求:
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { // 复用通用判断逻辑,减少重复代码 function isAuthenticated() { return request.auth != null; } // 管理员标记存在Users集合对应文档的isAdmin字段中,不要硬编码管理员uid function isAdmin() { return isAuthenticated() && exists(/databases/$(database)/documents/users/$(request.auth.uid)) && get(/databases/$(database)/documents/users/$(request.auth.uid)).data.isAdmin == true; } function isBookingOwner(bookingData) { return isAuthenticated() && request.auth.uid == bookingData.uid; } match /bookings/{bookingId} { // 读权限:管理员可访问全量,普通用户仅能查询带自己uid过滤条件的预约 allow read: if isAdmin() || (isBookingOwner(resource.data) && request.query.where.uid == request.auth.uid); // 写权限:管理员可操作所有,普通用户仅能创建/修改/删除自己的预约 allow create: if isAdmin() || isBookingOwner(request.resource.data); allow update, delete: if isAdmin() || isBookingOwner(resource.data); } match /users/{userId} { // 用户仅能读写自己的基础信息,管理员可操作所有用户数据 allow read, write: if isAdmin() || request.auth.uid == userId; } } }
注意:不要把管理员uid硬编码在规则中,通过Users集合的
isAdmin字段标记管理员,后续新增、移除管理员不需要重新发布规则。
2. 查询代码调整
普通用户查询时必须加和规则匹配的uid过滤条件,管理员查询全量数据时不加过滤:
Future<List<BookingModel>> fetchBookings({bool isAdmin = false}) async { QuerySnapshot bookingsSnapshot; if (isAdmin) { bookingsSnapshot = await FirebaseFirestore.instance.collection('bookings').get(); } else { final currentUid = FirebaseAuth.instance.currentUser!.uid; // 必须加uid过滤条件,和规则约束完全对应 bookingsSnapshot = await FirebaseFirestore.instance .collection('bookings') .where('uid', isEqualTo: currentUid) .get(); } return bookingsSnapshot.docs.map((snapshot) { final bookingMap = snapshot.data() as Map<String, dynamic>; return BookingModel( bookingMap['email'], bookingMap['location'], bookingMap['phoneNumber'], // Firestore时间字段默认是Timestamp类型,必须转成本地DateTime (bookingMap['dateTime'] as Timestamp).toDate(), bookingMap['uid'], (bookingMap['dateCreated'] as Timestamp).toDate() ); }).toList(); }
调整后就算前端代码被篡改,普通用户发起不带uid过滤的全量查询也会被规则直接拦截,不会出现数据泄露问题。
方案二:使用Users子集合存储预约(适合用户数据强隔离、极少跨用户查询的场景)
如果不需要频繁做全局预约统计,可以把每个用户的预约存在对应用户文档的子集合下,天然实现数据隔离,规则配置更简单。
1. 数据结构调整
- 保留Users集合存储用户基础信息
- 预约数据存储路径调整为
/users/{用户uid}/bookings/{预约id},迁移时直接把原根集合下的预约文档复制到对应用户的子集合即可,预约文档ID可以和原有ID保持一致,不需要修改其他关联逻辑。
2. 安全规则配置
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { function isAuthenticated() { return request.auth != null; } function isAdmin() { return isAuthenticated() && get(/databases/$(database)/documents/users/$(request.auth.uid)).data.isAdmin == true; } match /users/{userId} { allow read, write: if isAdmin() || request.auth.uid == userId; // 子集合下的预约天然和用户id绑定,权限判断更简单 match /bookings/{bookingId} { allow read, write: if isAdmin() || request.auth.uid == userId; } } } }
3. 查询代码
Future<List<BookingModel>> fetchBookings({bool isAdmin = false}) async { QuerySnapshot bookingsSnapshot; final currentUid = FirebaseAuth.instance.currentUser!.uid; if (isAdmin) { // 管理员拉全量预约用集合组查询 bookingsSnapshot = await FirebaseFirestore.instance.collectionGroup('bookings').get(); } else { // 普通用户直接查自己目录下的预约子集合 bookingsSnapshot = await FirebaseFirestore.instance .collection('users') .doc(currentUid) .collection('bookings') .get(); } return bookingsSnapshot.docs.map((snapshot) { final bookingMap = snapshot.data() as Map<String, dynamic>; return BookingModel( bookingMap['email'], bookingMap['location'], bookingMap['phoneNumber'], (bookingMap['dateTime'] as Timestamp).toDate(), bookingMap['uid'], (bookingMap['dateCreated'] as Timestamp).toDate() ); }).toList(); }
注意:第一次用集合组查询时控制台会报索引缺失错误,直接点击报错信息里的链接创建对应索引,等部署完成即可正常查询。
选型参考
- 管理员操作频繁、需要经常做全平台预约统计、多维度筛选预约的场景选方案一,根集合查询更灵活,不需要额外配置集合组索引。
- 用户数据隔离要求高、几乎不需要跨用户查询预约的场景选方案二,权限逻辑更简单,不容易写错规则。
避坑提醒
- 永远不要写
allow write: if true这类全开放规则,属于严重安全漏洞,会导致数据库被任意篡改。 - 所有查询必须在代码层面就加好对应过滤条件,不要依赖规则做数据筛选。
- 读取Firestore的时间类型字段时,必须手动将Timestamp转成Dart的DateTime类型,否则模型序列化会报错。
内容的提问来源于stack exchange,提问作者Joey Leo
相关产品推荐
相关产品推荐

