帮助用户和用户组确保数据访问的安全性

许多协作应用允许用户根据一组权限读取和写入不同的数据片段。例如,在文档编辑应用中,用户可能希望在阻止不必要的访问的同时,允许其他一些用户对其文档执行读写操作。

解决方案:基于角色的访问权限控制

您可以利用 Cloud Firestore 的数据模型以及自定义的安全规则在您的应用中实现基于角色的访问权限控制。

假设您正在构建一款协作式撰文应用,用户可以按照以下安全要求在其中撰写“故事”和“评论”:

  • 每个故事都有一名所有者;故事可共享给“撰写者”、“评论者”和“读者”。
  • “读者”只能查看故事和评论,不能编辑任何内容。
  • “评论者”除了拥有读者所拥有的全部访问权限之外,还可以就故事添加评论。
  • “撰写者”除了拥有评论者所拥有的全部访问权限之外,还可以编辑故事内容。
  • “所有者”可以编辑故事的任意部分,并且可以控制其他用户的访问权限。

数据结构

假设您的应用有一个 stories 集合,其中每个文档代表一个故事。每个故事还有一个 comments 子集合,其中每个文档代表一条针对该故事的评论。

为了跟踪访问角色,需要添加一个 roles 字段,用来将用户 ID 映射至角色:

/stories/{storyid}

{
  title: "A Great Story",
  content: "Once upon a time ...",
  roles: {
    alice: "owner",
    bob: "reader",
    david: "writer",
    jane: "commenter"
    // ...
  }
}

评论仅包含两个字段,留言者的用户 ID 和一些内容:

/stories/{storyid}/comments/{commentid}

{
  user: "alice",
  content: "I think this is a great story!"
}

规则

既然数据库中记录有用户角色,那么便需要编写安全规则来进行角色验证。下列规则假定应用使用的是 Firebase 身份验证,所以 request.auth.uid 变量为用户 ID。

第 1 步:先创建基本的规则文件,其中包含针对故事和评论的空白规则:

service cloud.firestore {
   match /databases/{database}/documents {
     match /stories/{story} {
         // TODO: Story rules go here...

         match /comments/{comment} {
            // TODO: Comment rules go here...
         }
     }
   }
}

第 2 步:添加一条简单的 write 规则,以便让所有者拥有对故事的完整控制权限。所定义的函数可帮助确定某位用户的角色以及新文档是否有效:

service cloud.firestore {
   match /databases/{database}/documents {
     match /stories/{story} {
        function isSignedIn() {
          return request.auth != null;
        }

        function getRole(rsc) {
          // Read from the "roles" map in the resource (rsc).
          return rsc.data.roles[request.auth.uid];
        }

        function isOneOfRoles(rsc, array) {
          // Determine if the user is one of an array of roles
          return isSignedIn() && (getRole(rsc) in array);
        }

        function isValidNewStory() {
          // Valid if story does not exist and the new story has the correct owner.
          return resource == null
            && request.resource.data.roles[request.auth.uid] == 'owner';
        }

        // Owners can read, write, and delete stories
        allow write: if isValidNewStory() || isOneOfRoles(resource, ['owner']);

         match /comments/{comment} {
            // ...
         }
     }
   }
}

第 3 步:编写规则,以允许任何角色的用户阅读故事和发表评论。使用上一步所定义的函数使规则简明易懂:

service cloud.firestore {
   match /databases/{database}/documents {
     match /stories/{story} {
        function isSignedIn() {
          return request.auth != null;
        }

        function getRole(rsc) {
          return rsc.data.roles[request.auth.uid];
        }

        function isOneOfRoles(rsc, array) {
          return isSignedIn() && (getRole(rsc) in array);
        }

        function isValidNewStory() {
          return resource == null
            && request.resource.data.roles[request.auth.uid] == 'owner';
        }

        allow write: if isValidNewStory() || isOneOfRoles(resource, ['owner']);

        // Any role can read stories.
        allow read: if isOneOfRoles(resource, ['owner', 'writer', 'commenter', 'reader']);

        match /comments/{comment} {
          // Any role can read comments.
          allow read: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
                                      ['owner', 'writer', 'commenter', 'reader']);
        }
     }
   }
}

第 4 步:允许故事撰写者、评论者和所有者发表评论。请注意,此规则还会验证评论的 owner 是否与发出请求的用户相匹配,以防用户覆盖彼此的评论:

service cloud.firestore {
   match /databases/{database}/documents {
     match /stories/{story} {
        function isSignedIn() {
          return request.auth != null;
        }

        function getRole(rsc) {
          return rsc.data.roles[request.auth.uid];
        }

        function isOneOfRoles(rsc, array) {
          return isSignedIn() && (getRole(rsc) in array);
        }

        function isValidNewStory() {
          return resource == null
            && request.resource.data.roles[request.auth.uid] == 'owner';
        }

        allow write: if isValidNewStory() || isOneOfRoles(resource, ['owner'])
        allow read: if isOneOfRoles(resource, ['owner', 'writer', 'commenter', 'reader']);

        match /comments/{comment} {
          allow read: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
                                      ['owner', 'writer', 'commenter', 'reader']);

          // Owners, writers, and commenters can create comments. The
          // user id in the comment document must match the requesting
          // user's id.
          //
          // Note: we have to use get() here to retrieve the story
          // document so that we can check the user's role.
          allow create: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
                                        ['owner', 'writer', 'commenter', 'reader'])
                        && request.resource.data.user == request.auth.uid;
        }
     }
   }
}

第 5 步:让撰写者能够编辑故事内容,但不能修改故事角色或更改文档的任何其他属性。这需要将故事的 write 规则分成独立的 create 规则、update 规则和 delete 规则,因为撰写者只能更新故事:

service cloud.firestore {
   match /databases/{database}/documents {
     match /stories/{story} {
        function isSignedIn() {
          return request.auth != null;
        }

        function getRole(rsc) {
          return rsc.data.roles[request.auth.uid];
        }

        function isOneOfRoles(rsc, array) {
          return isSignedIn() && (getRole(rsc) in array);
        }

        function isValidNewStory() {
          return request.resource.data.roles[request.auth.uid] == 'owner';
        }

        function onlyContentChanged() {
          // Ensure that title and roles are unchanged and that no new
          // fields are added to the document.
          return request.resource.data.title == resource.data.title
            && request.resource.data.roles == resource.data.roles
            && request.resource.size() == resource.size();
        }

        // Split writing into creation, deletion, and updating. Only an
        // owner can create or delete a story but a writer can update
        // story content.
        allow create: if isValidNewStory();
        allow delete: if isOneOfRoles(resource, ['owner']);
        allow update: if isOneOfRoles(resource, ['owner'])
                      || (isOneOfRoles(resource, ['writer']) && onlyContentChanged());
        allow read: if isOneOfRoles(['owner', 'writer', 'commenter', 'reader']);

        match /comments/{comment} {
          allow read: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
                                      ['owner', 'writer', 'commenter', 'reader']);
          allow create: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
                                        ['owner', 'writer', 'commenter', 'reader'])
                        && request.resource.data.user == request.auth.uid;
        }
     }
   }
}

限制

上述解决方案演示了如何利用安全规则来确保用户数据的安全,但您应了解以下限制:

  • 细化:在上述示例中,多个角色(写入者和所有者)拥有同一文档的写入权限,但对应的限制不同。这对于较复杂的文档而言可能难以管理,将单个复杂文档拆分成多个简单文档,每个文档归单个角色所拥有,可能会是更好的做法。
  • 大型用户组:如果您需要与非常庞大或复杂的用户组进行共享,不妨考虑采用这样的系统:角色存储在其各自的集合中,而不是作为目标文档中的一个字段。

发送以下问题的反馈:

此网页
需要帮助?请访问我们的支持页面