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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 07:27:15